Merge branch '23-05-06-webgpu' into 25-10-04-claudia

Well I did my best solving conflicts but it sure as hell doesn't
compile.
This commit is contained in:
PJB3005
2025-10-05 16:07:58 +02:00
77 changed files with 4845 additions and 3974 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,66 @@
// Group 0: global constants.
struct UniformConstants {
time: f32
}
@group(0) @binding(0) var<uniform> Constants: UniformConstants;
// Group 1: parameters that change infrequently in a draw pass.
struct UniformView {
projViewMatrix: mat3x2f,
screenPixelSize: vec2f
}
@group(1) @binding(0) var<uniform> View: UniformView;
// Group 2: per-draw parameters.
@group(2) @binding(0)
var mainTexture: texture_2d<f32>;
@group(2) @binding(1)
var mainSampler: sampler;
struct VertexInput {
@location(0) position: vec2f,
@location(1) texCoord: vec2f,
@location(2) color: vec4f
}
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) texCoord: vec2f,
@location(1) color: vec4f,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var transformed = View.projViewMatrix * vec3(input.position, 1.0);
transformed += 1.0;
transformed /= View.screenPixelSize * 2.0;
transformed = floor(transformed + 0.5);
transformed *= View.screenPixelSize * 2.0;
transformed -= 1.0;
var out: VertexOutput;
out.position = vec4(transformed, 0.0, 1.0);
out.texCoord = input.texCoord;
out.color = srgb_to_linear(input.color);
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4f {
var color = textureSample(mainTexture, mainSampler, input.texCoord);
color = color * input.color;
return color;
}
fn srgb_to_linear(srgb: vec4f) -> vec4f {
let higher = pow((srgb.rgb + 0.055) / 1.055, vec3(2.4));
let lower = srgb.rgb / 12.92;
let s = max(vec3(0.0), sign(srgb.rgb - 0.04045));
return vec4(mix(lower, higher, s), srgb.a);
}

View File

@@ -32,14 +32,5 @@ namespace Robust.Client.Graphics
/// Our sub region within our source, in pixel coordinates.
/// </summary>
public UIBox2 SubRegion { get; }
public override Color GetPixel(int x, int y)
{
DebugTools.Assert(x < SubRegion.Right);
DebugTools.Assert(y < SubRegion.Top);
int xTranslated = x + (int) SubRegion.Left;
int yTranslated = y + (int) SubRegion.Top;
return this.SourceTexture[xTranslated, yTranslated];
}
}
}

View File

@@ -30,22 +30,5 @@ namespace Robust.Client.Graphics.Clyde
// To be clear: You shouldn't change this. This just helps with understanding where Primitive Restart is being used.
private const ushort PrimitiveRestartIndex = ushort.MaxValue;
private enum Renderer : sbyte
{
// Auto: Try all supported renderers (not necessarily the renderers shown here)
Auto = default,
OpenGL = 1,
Explode = -1,
}
private enum RendererOpenGLVersion : byte
{
Auto = default,
GL33 = 1,
GL31 = 2,
GLES3 = 3,
GLES2 = 4,
}
}
}

View File

@@ -3,14 +3,9 @@ namespace Robust.Client.Graphics.Clyde
internal sealed partial class Clyde
{
private readonly ClydeDebugStats _debugStats = new();
private ClydeDebugInfo? _debugInfo;
private sealed record ClydeDebugInfo(
OpenGLVersion OpenGLVersion,
string Renderer,
string Vendor,
string VersionString,
bool Overriding,
string WindowingApi) : IClydeDebugInfo;
private sealed record ClydeDebugInfo(string WindowingApi) : IClydeDebugInfo;
private sealed class ClydeDebugStats : IClydeDebugStats
{

View File

@@ -6,7 +6,6 @@ using Robust.Shared.Log;
#endif
using System.Collections.Generic;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Input;
using Robust.Shared.Maths;
@@ -112,10 +111,10 @@ namespace Robust.Client.Graphics.Clyde
if (!reg.IsVisible) // Only send this for open windows
return;
var loaded = RtToLoaded(reg.RenderTarget);
loaded.Size = reg.FramebufferSize;
// var loaded = RtToLoaded(reg.RenderTarget);
// loaded.Size = reg.FramebufferSize;
_glContext!.WindowResized(reg, oldSize);
Rhi.WindowRecreateSwapchain(reg);
var eventArgs = new WindowResizedEventArgs(
oldSize,

View File

@@ -1,80 +0,0 @@
using System;
using Robust.Shared;
namespace Robust.Client.Graphics.Clyde
{
internal sealed partial class Clyde
{
private GLContextBase? _glContext;
// Current OpenGL version we managed to initialize with.
private RendererOpenGLVersion _openGLVersion;
private void InitGLContextManager()
{
// Advanced GL contexts currently disabled due to lack of testing etc.
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngle))
{
if (_cfg.GetCVar(CVars.DisplayAngleCustomSwapChain))
{
_sawmillOgl.Debug("Trying custom swap chain ANGLE.");
var ctxAngle = new GLContextAngle(this);
if (ctxAngle.TryInitialize())
{
_sawmillOgl.Debug("Successfully initialized custom ANGLE");
_glContext = ctxAngle;
ctxAngle.EarlyInit();
return;
}
}
if (_cfg.GetCVar(CVars.DisplayEgl))
{
_sawmillOgl.Debug("Trying EGL");
var ctxEgl = new GLContextEgl(this);
ctxEgl.InitializePublic();
_glContext = ctxEgl;
return;
}
}
/*
if (OperatingSystem.IsLinux() && _cfg.GetCVar(CVars.DisplayEgl))
{
_sawmillOgl.Debug("Trying EGL");
var ctxEgl = new GLContextEgl(this);
ctxEgl.InitializePublic();
_glContext = ctxEgl;
return;
}
*/
_glContext = new GLContextWindow(this);
}
private struct GLContextSpec
{
public int Major;
public int Minor;
public GLContextProfile Profile;
public GLContextCreationApi CreationApi;
// Used by GLContextWindow to figure out which GL version managed to initialize.
public RendererOpenGLVersion OpenGLVersion;
}
private enum GLContextProfile
{
Compatibility,
Core,
Es
}
private enum GLContextCreationApi
{
Native,
Egl,
}
}
}

View File

@@ -1,12 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using OpenToolkit.Graphics.OpenGL4;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
// OpenGL feature detection go here.
/*// OpenGL feature detection go here.
private bool _hasGLKhrDebug;
private bool _glDebuggerPresent;
@@ -224,6 +220,6 @@ namespace Robust.Client.Graphics.Clyde
_sawmillOgl.Debug("OpenGL Extensions: {0}", extensions);
return new HashSet<string>(extensions.Split(' '));
}
}
}*/
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
@@ -16,6 +15,8 @@ namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/*
private readonly Dictionary<EntityUid, Dictionary<Vector2i, MapChunkData>> _mapChunkData =
new();
@@ -528,5 +529,6 @@ namespace Robust.Client.Graphics.Clyde
{
}
}
*/
}
}

View File

@@ -3,8 +3,8 @@ using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
@@ -15,6 +15,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde
@@ -34,9 +35,55 @@ namespace Robust.Client.Graphics.Clyde
private List<Overlay> _overlays = new();
private SpriteBatch? _spriteBatch;
private void RenderInit()
{
LoadStockTextures();
_spriteBatch = new SpriteBatch(this, Rhi);
_renderHandle = new RenderHandle(this, _entityManager, _spriteBatch);
}
private void DrawSplash()
{
var splashTex = _cfg.GetCVar(CVars.DisplaySplashLogo);
if (string.IsNullOrEmpty(splashTex))
return;
var backbuffer = _mainWindow!.CurSwapchainView!;
var size = _mainWindow!.FramebufferSize;
_spriteBatch!.Start();
_spriteBatch.BeginPass(size, backbuffer);
var texture = _resourceCache.GetResource<TextureResource>(splashTex).Texture;
var pos = (size - texture.Size) / 2;
_spriteBatch.Draw((ClydeTexture) texture, pos, Color.White);
_spriteBatch.EndPass();
_spriteBatch.Finish();
}
public void Render()
{
CheckTransferringScreenshots();
AcquireSwapchainTextures();
try
{
RenderCore();
}
finally
{
// wgpu will be very annoyed if we don't do the present state machine correctly.
// So make sure we ALWAYS present.
PresentWindows();
}
/*CheckTransferringScreenshots();
var allMinimized = true;
foreach (var windowReg in _windows)
@@ -118,7 +165,20 @@ namespace Robust.Client.Graphics.Clyde
_prof.WriteValue("Lights", ProfData.Int32(_debugStats.TotalLights));
_prof.WriteValue("Shadow Lights", ProfData.Int32(_debugStats.ShadowLights));
_prof.WriteValue("Occluders", ProfData.Int32(_debugStats.Occluders));
}*/
}
private void RenderCore()
{
if (_drawingSplash)
{
DrawSplash();
return;
}
_spriteBatch!.Start();
_userInterfaceManager.Render(_renderHandle);
_spriteBatch.Finish();
}
public void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback)
@@ -134,6 +194,7 @@ namespace Robust.Client.Graphics.Clyde
null);
}
/*
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
// Check that entity manager has started.
@@ -616,5 +677,6 @@ namespace Robust.Client.Graphics.Clyde
return new Box2Rotated(aabb, rotation, aabb.Center);
}
*/
}
}

View File

@@ -1,17 +1,8 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using ES20 = OpenToolkit.Graphics.ES20;
namespace Robust.Client.Graphics.Clyde
{
internal sealed partial class Clyde
{
/*
private void GLClearColor(Color color)
{
GL.ClearColor(color.R, color.G, color.B, color.A);
@@ -290,5 +281,7 @@ namespace Robust.Client.Graphics.Clyde
return proc;
}
*/
}
}

View File

@@ -1,152 +0,0 @@
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Clyde
{
// Contains various layout/rendering structs used inside Clyde.
internal partial class Clyde
{
/// <summary>
/// Sets up VAO layout for Vertex2D for base and raw shader types.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void SetupVAOLayout()
{
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Texture Coords (2).
GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
// Colour Modulation.
GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
GL.EnableVertexAttribArray(3);
}
// NOTE: This is:
// + Directly cast from DrawVertexUV2DColor!!!
// + GLContextWindow does it's own thing with this for winblit, be careful!
[StructLayout(LayoutKind.Sequential)]
[PublicAPI]
private readonly struct Vertex2D
{
public readonly Vector2 Position;
public readonly Vector2 TextureCoordinates;
public readonly Vector2 TextureCoordinates2;
// Note that this color is in linear space.
public readonly Color Modulate;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Color modulate)
{
Position = position;
TextureCoordinates = textureCoordinates;
Modulate = modulate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
{
Position = position;
TextureCoordinates = textureCoordinates;
TextureCoordinates2 = textureCoordinates2;
Modulate = modulate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(float x, float y, float u, float v, Color modulate)
: this(new Vector2(x, y), new Vector2(u, v), modulate)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(Vector2 position, float u, float v, Color modulate)
: this(position, new Vector2(u, v), modulate)
{
}
public override string ToString()
{
return $"Vertex2D: {Position}, {TextureCoordinates}, {Modulate}";
}
}
[StructLayout(LayoutKind.Explicit, Size = 28 * sizeof(float))]
[PublicAPI]
private struct ProjViewMatrices : IAppliableUniformSet
{
[FieldOffset(0 * sizeof(float))] public Vector3 ProjMatrixC0;
[FieldOffset(4 * sizeof(float))] public Vector3 ProjMatrixC1;
[FieldOffset(8 * sizeof(float))] public Vector3 ProjMatrixC2;
[FieldOffset(12 * sizeof(float))] public Vector3 ViewMatrixC0;
[FieldOffset(16 * sizeof(float))] public Vector3 ViewMatrixC1;
[FieldOffset(20 * sizeof(float))] public Vector3 ViewMatrixC2;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ProjViewMatrices(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix)
{
// We put the rows of the input matrix into the columns of our GPU matrices
// this transpose is required, as in C#, we premultiply vectors with matrices
// (vM) while GL postmultiplies vectors with matrices (Mv); however, since
// the Matrix3x2 data is stored row-major, and GL uses column-major, the
// memory layout is the same (or would be, if Matrix3x2 didn't have an
// implicit column)
ProjMatrixC0 = new Vector3(projMatrix.M11, projMatrix.M12, 0);
ProjMatrixC1 = new Vector3(projMatrix.M21, projMatrix.M22, 0);
ProjMatrixC2 = new Vector3(projMatrix.M31, projMatrix.M32, 1);
ViewMatrixC0 = new Vector3(viewMatrix.M11, viewMatrix.M12, 0);
ViewMatrixC1 = new Vector3(viewMatrix.M21, viewMatrix.M22, 0);
ViewMatrixC2 = new Vector3(viewMatrix.M31, viewMatrix.M32, 1);
}
public void Apply(Clyde clyde, GLShaderProgram program)
{
program.SetUniformMaybe("projectionMatrix", new Matrix3x2(
ProjMatrixC0.X, ProjMatrixC0.Y, // Implicit 0
ProjMatrixC1.X, ProjMatrixC1.Y, // Implicit 0
ProjMatrixC2.X, ProjMatrixC2.Y // Implicit 1
));
program.SetUniformMaybe("viewMatrix", new Matrix3x2(
ViewMatrixC0.X, ViewMatrixC0.Y, // Implicit 0
ViewMatrixC1.X, ViewMatrixC1.Y, // Implicit 0
ViewMatrixC2.X, ViewMatrixC2.Y // Implicit 1
));
}
}
[StructLayout(LayoutKind.Explicit, Size = sizeof(float) * 4)]
[PublicAPI]
private struct UniformConstants : IAppliableUniformSet
{
[FieldOffset(0)] public Vector2 ScreenPixelSize;
[FieldOffset(2 * sizeof(float))] public float Time;
public UniformConstants(Vector2 screenPixelSize, float time)
{
ScreenPixelSize = screenPixelSize;
Time = time;
}
public void Apply(Clyde clyde, GLShaderProgram program)
{
program.SetUniformMaybe("SCREEN_PIXEL_SIZE", ScreenPixelSize);
program.SetUniformMaybe("TIME", Time);
}
}
}
}

View File

@@ -1,17 +1,16 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using Robust.Client.GameObjects;
using System.Diagnostics.Contracts;
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Robust.Shared.Physics;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
@@ -28,6 +27,7 @@ namespace Robust.Client.Graphics.Clyde
internal partial class Clyde
{
/*
// Horizontal width, in pixels, of the shadow maps used to render regular lights.
private const int ShadowMapSize = 512;
@@ -93,12 +93,14 @@ namespace Robust.Client.Graphics.Clyde
private ClydeTexture ShadowTexture => _shadowRenderTarget.Texture;
private (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)[] _lightsToRenderList = default!;
*/
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private float _maxLightRadius;
/*
private unsafe void InitLighting()
{
_cfg.OnValueChanged(CVars.MaxLightRadius, val => { _maxLightRadius = val;}, true);
@@ -1271,5 +1273,6 @@ namespace Robust.Client.Graphics.Clyde
_lightsToRenderList = new (PointLightComponent, Vector2, float , Angle)[value];
DebugTools.Assert(_maxLights >= _maxShadowcastingLights);
}
*/
}
}

View File

@@ -2,9 +2,10 @@ using System;
using System.Numerics;
using System.Runtime.InteropServices;
using Robust.Client.GameObjects;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using OpenToolkit.Graphics.OpenGL4;
using Vector2 = Robust.Shared.Maths.Vector2;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics.Clyde
@@ -19,14 +20,16 @@ namespace Robust.Client.Graphics.Clyde
{
private readonly Clyde _clyde;
private readonly IEntityManager _entities;
private readonly SpriteBatch _spriteBatch;
public DrawingHandleScreen DrawingHandleScreen { get; }
public DrawingHandleWorld DrawingHandleWorld { get; }
public RenderHandle(Clyde clyde, IEntityManager entities)
public RenderHandle(Clyde clyde, IEntityManager entities, SpriteBatch spriteBatch)
{
_clyde = clyde;
_entities = entities;
_spriteBatch = spriteBatch;
var white = _clyde.GetStockTexture(ClydeStockTexture.White);
DrawingHandleScreen = new DrawingHandleScreenImpl(white, this);
@@ -35,7 +38,8 @@ namespace Robust.Client.Graphics.Clyde
public void SetModelTransform(in Matrix3x2 matrix)
{
_clyde.DrawSetModelTransform(matrix);
matrix.Transpose(out var transposed);
_spriteBatch.SetModelTransform((Matrix3x2)transposed);
}
public Matrix3x2 GetModelTransform()
@@ -45,7 +49,7 @@ namespace Robust.Client.Graphics.Clyde
public void SetProjView(in Matrix3x2 proj, in Matrix3x2 view)
{
_clyde.DrawSetProjViewTransform(proj, view);
// _clyde.DrawSetProjViewTransform(proj, view);
}
/// <summary>
@@ -68,7 +72,8 @@ namespace Robust.Client.Graphics.Clyde
var (w, h) = clydeTexture.Size;
var sr = new Box2(csr.Left / w, (h - csr.Top) / h, csr.Right / w, (h - csr.Bottom) / h);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
_spriteBatch.Draw(clydeTexture, bl, br, tl, tr, modulate, sr);
// _clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
/// <summary>
@@ -90,7 +95,7 @@ namespace Robust.Client.Graphics.Clyde
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
// _clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
@@ -130,12 +135,52 @@ namespace Robust.Client.Graphics.Clyde
public void RenderInRenderTarget(IRenderTarget target, Action a, Color? clearColor)
{
_clyde.RenderInRenderTarget((RenderTargetBase) target, a, clearColor);
// TODO: Save/restore SpriteBatch state
RhiTextureView targetTexture;
RhiTextureView? depthView = null;
Vector2i targetSize;
var loaded = _clyde.RtToLoaded((RenderTargetBase)target);
if (loaded.IsWindow)
{
targetTexture = loaded.Window!.CurSwapchainView!;
targetSize = loaded.Window!.FramebufferSize;
}
else
{
var loadedTex = _clyde._loadedTextures[loaded.TextureHandle];
targetTexture = loadedTex.DefaultRhiView;
depthView = loaded.DepthSencilTextureView;
targetSize = loaded.Size;
}
_spriteBatch.BeginPass(targetSize, targetTexture, clearColor);
a();
_spriteBatch.EndPass();
}
public void SetScissor(UIBox2i? scissorBox)
{
_clyde.DrawSetScissor(scissorBox);
if (scissorBox is { } box)
{
var (targetWidth, targetHeight) = _spriteBatch.CurrentTargetSize;
_spriteBatch.SetScissor(
Math.Max(0, box.Left),
Math.Max(0, box.Top),
Math.Min(targetWidth, box.Width),
Math.Min(targetHeight, box.Height)
);
}
else
{
_spriteBatch.ClearScissor();
}
}
/// <summary>
@@ -165,6 +210,7 @@ namespace Robust.Client.Graphics.Clyde
TransformComponent? xform = null,
SharedTransformSystem? xformSystem = null)
{
/*
if (_entities.Deleted(entity))
{
throw new ArgumentException("Tried to draw an entity has been deleted.", nameof(entity));
@@ -216,11 +262,12 @@ namespace Robust.Client.Graphics.Clyde
// Reset to screen space
SetProjView(oldProj, oldView);
SetModelTransform(oldModel);
*/
}
public void DrawLine(Vector2 a, Vector2 b, Color color)
{
_clyde.DrawLine(a, b, color);
// _clyde.DrawLine(a, b, color);
}
public void UseShader(ShaderInstance? shader)
@@ -232,7 +279,7 @@ namespace Robust.Client.Graphics.Clyde
var clydeShader = (ClydeShaderInstance?) shader;
_clyde.DrawUseShader(clydeShader ?? _clyde._defaultShader);
// _clyde.DrawUseShader(clydeShader?.Handle ?? _clyde._defaultShader.Handle);
}
public ShaderInstance? GetShader()
@@ -244,24 +291,27 @@ namespace Robust.Client.Graphics.Clyde
public void Viewport(Box2i viewport)
{
_clyde.DrawViewport(viewport);
// _clyde.DrawViewport(viewport);
}
public void UseRenderTarget(IRenderTarget? renderTarget)
{
var target = (RenderTexture?) renderTarget;
/*var target = (RenderTexture?) renderTarget;
_clyde.DrawRenderTarget(target?.Handle ?? default);
_clyde.DrawRenderTarget(target?.Handle ?? default);*/
}
/*
public void Clear(Color color, int stencil = 0, ClearBufferMask mask = ClearBufferMask.ColorBufferBit)
{
_clyde.DrawClear(color, stencil, mask);
}
*/
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
ReadOnlySpan<DrawVertexUV2DColor> vertices)
{
/*
if (!(texture is ClydeTexture clydeTexture))
{
throw new ArgumentException("Texture must be a basic texture.");
@@ -270,20 +320,21 @@ namespace Robust.Client.Graphics.Clyde
var castSpan = MemoryMarshal.Cast<DrawVertexUV2DColor, Vertex2D>(vertices);
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, castSpan);
*/
}
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
ReadOnlySpan<ushort> indices,
ReadOnlySpan<DrawVertexUV2DColor> vertices)
{
if (!(texture is ClydeTexture clydeTexture))
/*if (!(texture is ClydeTexture clydeTexture))
{
throw new ArgumentException("Texture must be a basic texture.");
}
var castSpan = MemoryMarshal.Cast<DrawVertexUV2DColor, Vertex2D>(vertices);
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, indices, castSpan);
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, indices, castSpan);*/
}
// ---- (end) ----

View File

@@ -1,34 +1,23 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
// ReSharper disable once IdentifierTypo
using RTCF = Robust.Client.Graphics.RenderTargetColorFormat;
using PIF = OpenToolkit.Graphics.OpenGL4.PixelInternalFormat;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private readonly Dictionary<ClydeHandle, LoadedRenderTarget> _renderTargets =
new();
private readonly Dictionary<ClydeHandle, LoadedRenderTarget> _renderTargets = new();
private readonly ConcurrentQueue<ClydeHandle> _renderTargetDisposeQueue
= new();
// This is always kept up-to-date, except in CreateRenderTarget (because it restores the old value)
// It is used for SRGB emulation.
// It, like _mainWindowRenderTarget, is initialized in Clyde's constructor
private LoadedRenderTarget _currentBoundRenderTarget;
// private readonly ConcurrentQueue<ClydeHandle> _renderTargetDisposeQueue = new();
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
@@ -56,176 +45,77 @@ namespace Robust.Client.Graphics.Clyde
DebugTools.Assert(size.X != 0);
DebugTools.Assert(size.Y != 0);
// Cache currently bound framebuffers
// so if somebody creates a framebuffer while drawing it won't ruin everything.
// Note that this means _currentBoundRenderTarget goes temporarily out of sync here
var boundDrawBuffer = GL.GetInteger(
_isGLES2 ? GetPName.FramebufferBinding : GetPName.DrawFramebufferBinding);
var boundReadBuffer = 0;
if (_hasGLReadFramebuffer)
{
boundReadBuffer = GL.GetInteger(GetPName.ReadFramebufferBinding);
}
// Generate FBO.
var fbo = new GLHandle(GL.GenFramebuffer());
// Bind color attachment to FBO.
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fbo.Handle);
CheckGlError();
ObjectLabelMaybe(ObjectLabelIdentifier.Framebuffer, fbo, name);
var (width, height) = size;
ClydeTexture textureObject;
GLHandle depthStencilBuffer = default;
var estPixSize = 0L;
RhiTexture? depthStencilTexture = null;
RhiTextureView? depthStencilTextureView = null;
// Color attachment.
{
var texture = new GLHandle(GL.GenTexture());
CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
CheckGlError();
ApplySampleParameters(sampleParameters);
var colorFormat = format.ColorFormat;
if ((!_hasGLSrgb) && (colorFormat == RTCF.Rgba8Srgb))
var textureFormat = format.ColorFormat switch
{
// If SRGB is not supported, switch formats.
// The shaders will have to compensate.
// Note that a check is performed on the *original* format.
colorFormat = RTCF.Rgba8;
}
// This isn't good
if (!_hasGLFloatFramebuffers)
{
switch (colorFormat)
{
case RTCF.R32F:
case RTCF.RG32F:
case RTCF.R11FG11FB10F:
case RTCF.Rgba16F:
_sawmillOgl.Warning("The framebuffer {0} [{1}] is trying to be floating-point when that's not supported. Forcing Rgba8.", name == null ? "[unnamed]" : name, size);
colorFormat = RTCF.Rgba8;
break;
}
}
// Make sure to specify the correct pixel type and formats even if we're not uploading any data.
// Not doing this (just sending red/byte) is fine on desktop GL but illegal on ES.
// @formatter:off
var (internalFormat, pixFormat, pixType) = colorFormat switch
{
RTCF.Rgba8 => (PIF.Rgba8, PF.Rgba, PT.UnsignedByte),
RTCF.Rgba16F => (PIF.Rgba16f, PF.Rgba, PT.Float),
RTCF.Rgba8Srgb => (PIF.Srgb8Alpha8, PF.Rgba, PT.UnsignedByte),
RTCF.R11FG11FB10F => (PIF.R11fG11fB10f, PF.Rgb, PT.Float),
RTCF.R32F => (PIF.R32f, PF.Red, PT.Float),
RTCF.RG32F => (PIF.Rg32f, PF.Rg, PT.Float),
RTCF.R8 => (PIF.R8, PF.Red, PT.UnsignedByte),
_ => throw new ArgumentOutOfRangeException(nameof(format.ColorFormat), format.ColorFormat, null)
RenderTargetColorFormat.Rgba8 => RhiTextureFormat.BGRA8Unorm,
RenderTargetColorFormat.Rgba8Srgb => RhiTextureFormat.BGRA8UnormSrgb,
RenderTargetColorFormat.R32F => RhiTextureFormat.R32Float,
RenderTargetColorFormat.RG32F => RhiTextureFormat.RG32Float,
RenderTargetColorFormat.Rgba16F => RhiTextureFormat.RGBA16Float,
RenderTargetColorFormat.R11FG11FB10F => RhiTextureFormat.RG11B10Ufloat,
RenderTargetColorFormat.R8 => RhiTextureFormat.R8Unorm,
_ => throw new ArgumentOutOfRangeException()
};
// @formatter:on
if (_isGLES2)
{
(internalFormat, pixFormat, pixType) = colorFormat switch
{
RTCF.Rgba8 => (PIF.Rgba, PF.Rgba, PT.UnsignedByte),
RTCF.R8 => (PIF.Rgba, PF.Rgba, PT.UnsignedByte),
_ => throw new ArgumentOutOfRangeException(nameof(format.ColorFormat), format.ColorFormat, null)
};
}
estPixSize += EstPixelSize(internalFormat);
GL.TexImage2D(TextureTarget.Texture2D, 0, internalFormat, width, height, 0, pixFormat,
pixType, IntPtr.Zero);
CheckGlError();
if (!_isGLES)
{
GL.FramebufferTexture(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0,
texture.Handle,
0);
}
else
{
// OpenGL ES uses a different name, and has an odd added target argument
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0,
TextureTarget.Texture2D, texture.Handle, 0);
}
CheckGlError();
// Check on original format is NOT a bug, this is so srgb emulation works
textureObject = GenTexture(texture, size, format.ColorFormat == RTCF.Rgba8Srgb, name == null ? null : $"{name}-color", TexturePixelType.RenderTarget);
(textureObject, _) = CreateBlankTextureCore(
size,
name != null ? $"RT_{name}_color" : null,
textureFormat,
sampleParameters ?? TextureSampleParameters.Default,
format.ColorFormat == RenderTargetColorFormat.Rgba8Srgb
);
}
// Depth/stencil buffers.
if (format.HasDepthStencil)
{
depthStencilBuffer = new GLHandle(GL.GenRenderbuffer());
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthStencilBuffer.Handle);
CheckGlError();
var depthLabel = name != null ? $"RT_{name}_depthStencil" : null;
ObjectLabelMaybe(ObjectLabelIdentifier.Renderbuffer, depthStencilBuffer,
name == null ? null : $"{name}-depth-stencil");
depthStencilTexture = Rhi.CreateTexture(new RhiTextureDescriptor(
new RhiExtent3D(size.X, size.Y),
RhiTextureFormat.Depth24PlusStencil8,
RhiTextureUsage.RenderAttachment,
Label: depthLabel
));
GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.Depth24Stencil8, width,
height);
CheckGlError();
estPixSize += 4;
GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment,
RenderbufferTarget.Renderbuffer, depthStencilBuffer.Handle);
GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.StencilAttachment,
RenderbufferTarget.Renderbuffer, depthStencilBuffer.Handle);
CheckGlError();
depthStencilTextureView = depthStencilTexture.CreateView(new RhiTextureViewDescriptor
{
Aspect = RhiTextureAspect.All,
Dimension = RhiTextureViewDimension.Dim2D,
Format = RhiTextureFormat.Depth24PlusStencil8,
Label = depthLabel,
MipLevelCount = 1,
ArrayLayerCount = 1,
BaseArrayLayer = 0,
BaseMipLevel = 0
});
}
// This should always pass but OpenGL makes it easy to check for once so let's.
var status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
CheckGlError();
DebugTools.Assert(status == FramebufferErrorCode.FramebufferComplete,
$"new framebuffer has bad status {status}");
// Re-bind previous framebuffers (thus _currentBoundRenderTarget is back in sync)
GL.BindFramebuffer(
_isGLES2 ? FramebufferTarget.Framebuffer : FramebufferTarget.DrawFramebuffer,
boundDrawBuffer);
CheckGlError();
if (_hasGLReadFramebuffer)
{
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, boundReadBuffer);
CheckGlError();
}
var pressure = estPixSize * size.X * size.Y;
var handle = AllocRid();
var renderTarget = new RenderTexture(size, textureObject, this, handle);
var data = new LoadedRenderTarget
{
IsWindow = false,
IsSrgb = textureObject.IsSrgb,
DepthStencilHandle = depthStencilBuffer,
FramebufferHandle = fbo,
Size = size,
TextureHandle = textureObject.TextureId,
MemoryPressure = pressure,
ColorFormat = format.ColorFormat,
DepthStencilTexture = depthStencilTexture,
DepthSencilTextureView = depthStencilTextureView
MemoryPressure = pressure,
SampleParameters = sampleParameters,
Instance = new WeakReference<RenderTargetBase>(renderTarget),
Name = name,
};
//GC.AddMemoryPressure(pressure);
var renderTarget = new RenderTexture(size, textureObject, this, handle);
_renderTargets.Add(handle, data);
return renderTarget;
}
@@ -233,38 +123,15 @@ namespace Robust.Client.Graphics.Clyde
private void DeleteRenderTexture(ClydeHandle handle)
{
if (!_renderTargets.TryGetValue(handle, out var renderTarget))
{
return;
}
DebugTools.Assert(renderTarget.FramebufferHandle != default);
DebugTools.Assert(!renderTarget.IsWindow, "Cannot delete window-backed render targets directly.");
GL.DeleteFramebuffer(renderTarget.FramebufferHandle.Handle);
renderTarget.FramebufferHandle = default;
CheckGlError();
_renderTargets.Remove(handle);
DeleteTexture(renderTarget.TextureHandle);
if (renderTarget.DepthStencilHandle != default)
{
GL.DeleteRenderbuffer(renderTarget.DepthStencilHandle.Handle);
CheckGlError();
}
//GC.RemoveMemoryPressure(renderTarget.MemoryPressure);
}
private void BindRenderTargetFull(LoadedRenderTarget rt)
{
BindRenderTargetImmediate(rt);
_currentRenderTarget = rt;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BindRenderTargetFull(RenderTargetBase rt)
{
BindRenderTargetFull(RtToLoaded(rt));
renderTarget.DepthStencilTexture?.Dispose();
renderTarget.DepthSencilTextureView?.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -279,22 +146,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BindRenderTargetImmediate(LoadedRenderTarget rt)
{
// NOTE: It's critically important that this be the "focal point" of all framebuffer bindings.
if (rt.IsWindow)
{
_glContext!.BindWindowRenderTarget(rt.WindowId);
}
else
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, rt.FramebufferHandle.Handle);
CheckGlError();
}
_currentBoundRenderTarget = rt;
}
/*
private void FlushRenderTargetDispose()
{
while (_renderTargetDisposeQueue.TryDequeue(out var handle))
@@ -302,6 +154,7 @@ namespace Robust.Client.Graphics.Clyde
DeleteRenderTexture(handle);
}
}
*/
public IEnumerable<(RenderTargetBase, LoadedRenderTarget)> GetLoadedRenderTextures()
{
@@ -317,8 +170,7 @@ namespace Robust.Client.Graphics.Clyde
internal sealed class LoadedRenderTarget
{
public bool IsWindow;
public WindowId WindowId;
public string? Name;
public WindowReg? Window;
public Vector2i Size;
public bool IsSrgb;
@@ -328,19 +180,12 @@ namespace Robust.Client.Graphics.Clyde
public RTCF ColorFormat;
// Remaining properties only apply if the render target is NOT a window.
// Handle to the framebuffer object.
public GLHandle FramebufferHandle;
// Handle to the loaded clyde texture managing the color attachment.
public ClydeHandle TextureHandle;
// Renderbuffer handle
public GLHandle DepthStencilHandle;
public long MemoryPressure;
public TextureSampleParameters? SampleParameters;
public required WeakReference<RenderTargetBase> Instance;
// Depth/stencil attachment.
public RhiTexture? DepthStencilTexture;
public RhiTextureView? DepthSencilTextureView;
}
internal abstract class RenderTargetBase : IRenderTarget
@@ -348,9 +193,6 @@ namespace Robust.Client.Graphics.Clyde
protected readonly Clyde Clyde;
private bool _disposed;
public bool MakeGLFence;
public nint LastGLSync;
protected RenderTargetBase(Clyde clyde, ClydeHandle handle)
{
Clyde = clyde;
@@ -359,9 +201,11 @@ namespace Robust.Client.Graphics.Clyde
public abstract Vector2i Size { get; }
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null)
where T : unmanaged, IPixel<T>
{
Clyde.CopyRenderTargetPixels(Handle, subRegion, callback);
throw new NotImplementedException();
//Clyde.CopyRenderTargetPixels(Handle, subRegion, callback);
}
public ClydeHandle Handle { get; }
@@ -396,7 +240,6 @@ namespace Robust.Client.Graphics.Clyde
protected virtual void DisposeDeferredImpl()
{
}
~RenderTargetBase()
@@ -432,11 +275,12 @@ namespace Robust.Client.Graphics.Clyde
protected override void DisposeDeferredImpl()
{
Clyde._renderTargetDisposeQueue.Enqueue(Handle);
throw new NotImplementedException();
// Clyde._renderTargetDisposeQueue.Enqueue(Handle);
}
}
private sealed class RenderWindow : RenderTargetBase
internal sealed class RenderWindow : RenderTargetBase
{
public override Vector2i Size => Clyde._renderTargets[Handle].Size;

View File

@@ -1,21 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/*
// The amount of quads we can render with ushort indices, leaving open 65536 for primitive restart.
private const ushort MaxBatchQuads = (2 << 13) - 1; // In human terms: (2**16/4)-1 = 16383
@@ -825,7 +822,7 @@ namespace Robust.Client.Graphics.Clyde
{
_batchLoadedTexture = null;
}
*/
#1#
}
private void FinishBatch()
@@ -1145,16 +1142,6 @@ namespace Robust.Client.Graphics.Clyde
GLCaps = glcaps;
}
}
[Flags]
private enum GLCaps : ushort
{
// If you add flags here make sure to update PopRenderState!
None = 0,
Blending = 1 << 0,
Stencilling = 1 << 2,
}
*/
}
}

View File

@@ -0,0 +1,67 @@
using System;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde;
internal sealed partial class Clyde
{
public RhiBase Rhi { get; private set; } = default!;
private void InitRhi()
{
DebugTools.Assert(_windowing != null);
DebugTools.Assert(_mainWindow != null);
var graphicsApi = _cfg.GetCVar(CVars.DisplayRhi);
_logManager.GetSawmill("clyde.rhi").Debug("Initializing RHI {RhiName}", graphicsApi);
Rhi = graphicsApi switch
{
"webGpu" => new RhiWebGpu(this, _deps),
_ => throw new Exception($"Unknown RHI: {graphicsApi}")
};
Rhi.Init();
}
private void AcquireSwapchainTextures()
{
// wgpu doesn't like it if we don't do anything with the swap chain images we acquire.
// To be safe, let's just always clear them every frame.
var encoder = Rhi.CreateCommandEncoder(new RhiCommandEncoderDescriptor("Clear acquired swap chains"));
foreach (var window in _windows)
{
window.CurSwapchainView?.Dispose();
window.CurSwapchainView = Rhi.CreateTextureViewForWindow(window);
var pass = encoder.BeginRenderPass(new RhiRenderPassDescriptor(
new[]
{
new RhiRenderPassColorAttachment(window.CurSwapchainView, RhiLoadOp.Clear, RhiStoreOp.Store)
}
));
pass.End();
}
Rhi.Queue.Submit(encoder.Finish());
}
private void PresentWindows()
{
foreach (var window in _windows)
{
if (window.CurSwapchainView == null)
return;
window.CurSwapchainView.Dispose();
window.CurSwapchainView = null;
Rhi.WindowPresent(window);
}
}
}

View File

@@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Utility;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
namespace Robust.Client.Graphics.Clyde
{
@@ -27,17 +24,19 @@ namespace Robust.Client.Graphics.Clyde
//
// For CopyPixels on render targets, the copy and transfer is started immediately when the function is called.
/*
private readonly List<QueuedScreenshot> _queuedScreenshots = new();
private readonly List<TransferringPixelCopy> _transferringPixelCopies = new();
*/
public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null)
{
_queuedScreenshots.Add(new QueuedScreenshot(type, callback, subRegion));
// _queuedScreenshots.Add(new QueuedScreenshot(type, callback, subRegion));
}
private void TakeScreenshot(ScreenshotType type)
{
if (_queuedScreenshots.Count == 0)
/*if (_queuedScreenshots.Count == 0)
{
return;
}
@@ -53,10 +52,10 @@ namespace Robust.Client.Graphics.Clyde
DoCopyPixels(ScreenSize, subRegion, callback);
_queuedScreenshots.RemoveSwap(i--);
}
}*/
}
private void CopyRenderTargetPixels<T>(
/*private void CopyRenderTargetPixels<T>(
ClydeHandle renderTarget,
UIBox2i? subRegion,
CopyPixelsDelegate<T> callback)
@@ -218,6 +217,6 @@ namespace Robust.Client.Graphics.Clyde
Vector2i Size,
// Funny callback dance to handle the generics.
Action<TransferringPixelCopy> TransferContinue,
Delegate Callback);
Delegate Callback);*/
}
}

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
@@ -41,8 +40,8 @@ namespace Robust.Client.Graphics.Clyde
private sealed class LoadedShader
{
[ViewVariables]
public GLShaderProgram Program = default!;
// [ViewVariables]
// public GLShaderProgram Program = default!;
[ViewVariables]
public string? Name;
@@ -89,7 +88,7 @@ namespace Robust.Client.Graphics.Clyde
public ClydeHandle LoadShader(ParsedShader shader, string? name = null, Dictionary<string,string>? defines = null)
{
var (vertBody, fragBody) = GetShaderCode(shader);
/*var (vertBody, fragBody) = GetShaderCode(shader);
var program = _compileProgram(vertBody, fragBody, BaseShaderAttribLocations, name, defines: defines);
@@ -106,12 +105,13 @@ namespace Robust.Client.Graphics.Clyde
};
var handle = AllocRid();
_loadedShaders.Add(handle, loaded);
return handle;
return handle;*/
return default;
}
public void ReloadShader(ClydeHandle handle, ParsedShader newShader)
{
var loaded = _loadedShaders[handle];
/*var loaded = _loadedShaders[handle];
var (vertBody, fragBody) = GetShaderCode(newShader);
@@ -125,7 +125,7 @@ namespace Robust.Client.Graphics.Clyde
{
program.BindBlock(UniProjViewMatrices, BindingIndexProjView);
program.BindBlock(UniUniformConstants, BindingIndexUniformConstants);
}
}*/
}
public ShaderInstance InstanceShader(ShaderSourceResource source, bool? lighting = null, ShaderBlendMode? mode = null)
@@ -142,6 +142,7 @@ namespace Robust.Client.Graphics.Clyde
return instance;
}
/*
private void LoadStockShaders()
{
_shaderLibrary = ReadEmbeddedShader("z-library.glsl");
@@ -162,6 +163,7 @@ namespace Robust.Client.Graphics.Clyde
_queuedShaderInstance = _defaultShader;
}
*/
private string ReadEmbeddedShader(string fileName)
{
@@ -172,6 +174,7 @@ namespace Robust.Client.Graphics.Clyde
return reader.ReadToEnd();
}
/*
private GLShaderProgram _compileProgram(string vertexSource, string fragmentSource,
(string, uint)[] attribLocations, string? name = null, bool includeLib=true, Dictionary<string,string>? defines=null)
{
@@ -279,6 +282,7 @@ namespace Robust.Client.Graphics.Clyde
fragmentShader?.Delete();
}
}
*/
private (string vertBody, string fragBody) GetShaderCode(ParsedShader shader)
{

View File

@@ -0,0 +1,578 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using RVector2 = Robust.Shared.Maths.Vector2;
using RVector4 = Robust.Shared.Maths.Vector4;
using SVector2 = System.Numerics.Vector2;
using SVector4 = System.Numerics.Vector4;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
// Implementation of a sprite batch on the new RHI.
// Basically, the old rendering API.
private sealed class SpriteBatch
{
private const int VertexSize = 32;
private const int UniformPassSize = 32;
private const uint BindGroup0 = 0;
private const uint BindGroup1 = 1;
private const uint BindGroup2 = 2;
private readonly Clyde _clyde;
private readonly RhiBase _rhi;
private readonly GpuExpansionBuffer _vertexBufferPool;
private readonly RhiBuffer _uniformConstantsBuffer;
private readonly GpuExpansionBuffer _uniformPassPool;
private readonly RhiRenderPipeline _pipeline;
private readonly RhiBindGroupLayout _group0Layout;
private readonly RhiBindGroupLayout _group1Layout;
private readonly RhiBindGroupLayout _group2Layout;
// Active recording state
private ValueList<RhiBindGroup> _tempBindGroups;
private RhiCommandEncoder? _commandEncoder;
private int _vertexStartIndex;
private RhiBuffer? _currentVertexBuffer;
private RhiRenderPassEncoder? _passEncoder;
private Vector2i _currentPassSize;
// Active batch state
private int _curBatchSize = 0;
private DirtyStateFlags _dirtyStateFlags;
private State<ClydeHandle> _stateTexture;
private State<(uint x, uint y, uint w, uint h)> _stateScissorRect;
private bool _scissorSkip;
// Other state
private Matrix3x2 _modelTransform;
public bool IsInRenderPass => _passEncoder != null;
public SpriteBatch(Clyde clyde, RhiBase rhi)
{
_clyde = clyde;
_rhi = rhi;
_vertexBufferPool = new GpuExpansionBuffer(
_rhi,
8192 * VertexSize,
RhiBufferUsageFlags.Vertex | RhiBufferUsageFlags.CopyDst,
label: "_vertexBuffer"
);
_uniformConstantsBuffer = _rhi.CreateBuffer(new RhiBufferDescriptor(
sizeof(float),
RhiBufferUsageFlags.Uniform,
Label: "_uniformConstantsBuffer"
));
var uniformPoolSize = MathHelper.CeilingPowerOfTwo(
UniformPassSize,
(int)_rhi.DeviceLimits.MinUniformBufferOffsetAlignment
) * 20;
_uniformPassPool = new GpuExpansionBuffer(
_rhi,
uniformPoolSize,
RhiBufferUsageFlags.Uniform | RhiBufferUsageFlags.CopyDst,
label: "_uniformPassBuffer");
var res = clyde._resourceCache;
var shaderSource = res.ContentFileReadAllText("/Shaders/Internal/default-sprite.wgsl");
using var shader = _rhi.CreateShaderModule(new RhiShaderModuleDescriptor(
shaderSource,
"default-sprite.wgsl"
));
_group0Layout = _rhi.CreateBindGroupLayout(new RhiBindGroupLayoutDescriptor(
new[]
{
new RhiBindGroupLayoutEntry(
0,
RhiShaderStage.Vertex | RhiShaderStage.Fragment,
new RhiBufferBindingLayout(MinBindingSize: 4)
)
},
"SpriteBatch bind group 0 (constants)"
));
_group1Layout = _rhi.CreateBindGroupLayout(new RhiBindGroupLayoutDescriptor(
new[]
{
new RhiBindGroupLayoutEntry(
0,
RhiShaderStage.Vertex | RhiShaderStage.Fragment,
new RhiBufferBindingLayout(MinBindingSize: 32)
)
},
"SpriteBatch bind group 1 (view)"
));
_group2Layout = _rhi.CreateBindGroupLayout(new RhiBindGroupLayoutDescriptor(
new[]
{
new RhiBindGroupLayoutEntry(
0,
RhiShaderStage.Fragment,
new RhiTextureBindingLayout()
),
new RhiBindGroupLayoutEntry(
1,
RhiShaderStage.Fragment,
new RhiSamplerBindingLayout()
)
},
"SpriteBatch bind group 2 (draw)"
));
var layout = _rhi.CreatePipelineLayout(new RhiPipelineLayoutDescriptor(
new[] { _group0Layout, _group1Layout, _group2Layout }, "SpriteBatch pipeline layout"
));
_pipeline = _rhi.CreateRenderPipeline(new RhiRenderPipelineDescriptor(
layout,
new RhiVertexState(
new RhiProgrammableStage(shader, "vs_main"),
new[]
{
new RhiVertexBufferLayout(32, RhiVertexStepMode.Vertex, new[]
{
new RhiVertexAttribute(RhiVertexFormat.Float32x2, 0, 0),
new RhiVertexAttribute(RhiVertexFormat.Float32x2, 8, 1),
new RhiVertexAttribute(RhiVertexFormat.Float32x4, 16, 2),
})
}
),
new RhiPrimitiveState(),
null,
new RhiMultisampleState(),
new RhiFragmentState(
new RhiProgrammableStage(shader, "fs_main"),
new[]
{
new RhiColorTargetState(
RhiTextureFormat.BGRA8UnormSrgb,
new RhiBlendState(
new RhiBlendComponent(
RhiBlendOperation.Add,
RhiBlendFactor.SrcAlpha,
RhiBlendFactor.OneMinusSrcAlpha
),
new RhiBlendComponent(RhiBlendOperation.Add, RhiBlendFactor.One, RhiBlendFactor.One)
)
)
}
),
"SpriteBatch pipeline"
));
}
// TODO: this name sucks
public Vector2i CurrentTargetSize => _currentPassSize;
public void Start()
{
Clear();
_commandEncoder = _rhi.CreateCommandEncoder(new RhiCommandEncoderDescriptor());
}
public void BeginPass(Vector2i size, RhiTextureView targetTexture, Color? clearColor = null)
{
ClearPassState();
_currentPassSize = size;
var projMatrix = default(Matrix3x2);
projMatrix.M11 = 2f / size.X;
projMatrix.M22 = -2f / size.Y;
projMatrix.M31 = -1;
projMatrix.M32 = 1;
var viewMatrix = Matrix3x2.Identity;
var projView = viewMatrix * projMatrix;
var uniformPass = _uniformPassPool.AllocateAligned<UniformView>(
1,
(int) _rhi.DeviceLimits.MinUniformBufferOffsetAlignment,
out var uniformPos
);
uniformPass[0] = new UniformView
{
ProjViewMatrix = ShaderMat3x2F.Transpose(projView),
ScreenPixelSize = new SVector2(1f / size.X, 1f / size.Y)
};
var rhiClearColor = clearColor == null ? new RhiColor(0, 0, 0, 1) : Color.FromSrgb(clearColor.Value);
_passEncoder = _commandEncoder!.BeginRenderPass(new RhiRenderPassDescriptor(
new[]
{
new RhiRenderPassColorAttachment(
targetTexture,
RhiLoadOp.Clear,
RhiStoreOp.Store,
ClearValue: rhiClearColor)
}
));
_passEncoder.SetPipeline(_pipeline);
var constantsGroup = AllocTempBindGroup(new RhiBindGroupDescriptor(
_group0Layout,
new[] { new RhiBindGroupEntry(0, new RhiBufferBinding(_uniformConstantsBuffer)) }
));
_passEncoder.SetBindGroup(BindGroup0, constantsGroup);
var passGroup = AllocTempBindGroup(new RhiBindGroupDescriptor(
_group1Layout,
new[]
{
new RhiBindGroupEntry(
0,
new RhiBufferBinding(uniformPos.Buffer, (ulong) uniformPos.ByteOffset, UniformPassSize)
)
}
));
_passEncoder.SetBindGroup(BindGroup1, passGroup);
}
public void EndPass()
{
FlushBatch();
_passEncoder!.End();
_passEncoder = null;
}
public void Finish()
{
DebugTools.Assert(_passEncoder == null, "Must end render pass before finishing the sprite batch!");
_uniformPassPool.Flush();
_vertexBufferPool.Flush();
var buffer = _commandEncoder!.Finish();
_rhi.Queue.Submit(buffer);
_commandEncoder = null;
Clear();
}
public void Draw(ClydeTexture texture, RVector2 position, Color color)
{
var textureHandle = texture.TextureId;
SetTexture(textureHandle);
ValidateBatchState();
var vertices = AllocateVertexSpace(6);
var width = texture.Width;
var height = texture.Height;
var bl = (SVector2)(position);
var br = (SVector2)(position + (width, 0));
var tr = (SVector2)(position + (width, height));
var tl = (SVector2)(position + (0, height));
var sBl = SVector2.Transform(bl, _modelTransform);
var sBr = SVector2.Transform(br, _modelTransform);
var sTl = SVector2.Transform(tl, _modelTransform);
var sTr = SVector2.Transform(tr, _modelTransform);
var asColor = Unsafe.As<Color, SVector4>(ref color);
vertices[0] = new Vertex2D(sBl, new SVector2(0, 1), asColor);
vertices[1] = new Vertex2D(sBr, new SVector2(1, 1), asColor);
vertices[2] = new Vertex2D(sTr, new SVector2(1, 0), asColor);
vertices[3] = new Vertex2D(sTr, new SVector2(1, 0), asColor);
vertices[4] = new Vertex2D(sTl, new SVector2(0, 0), asColor);
vertices[5] = new Vertex2D(sBl, new SVector2(0, 1), asColor);
_vertexStartIndex += 6;
_curBatchSize += 6;
}
public void Draw(
ClydeTexture texture,
RVector2 bl, RVector2 br, RVector2 tl, RVector2 tr,
in Color color,
in Box2 region)
{
var textureHandle = texture.TextureId;
SetTexture(textureHandle);
ValidateBatchState();
var vertices = AllocateVertexSpace(6);
var asColor = Unsafe.As<Color, SVector4>(ref Unsafe.AsRef(color));
var sBl = SVector2.Transform((SVector2)bl, _modelTransform);
var sBr = SVector2.Transform((SVector2)br, _modelTransform);
var sTl = SVector2.Transform((SVector2)tl, _modelTransform);
var sTr = SVector2.Transform((SVector2)tr, _modelTransform);
vertices[0] = new Vertex2D(sBl, (SVector2)region.BottomLeft, asColor);
vertices[1] = new Vertex2D(sBr, (SVector2)region.BottomRight, asColor);
vertices[2] = new Vertex2D(sTr, (SVector2)region.TopRight, asColor);
vertices[3] = vertices[2];
vertices[4] = new Vertex2D(sTl, (SVector2)region.TopLeft, asColor);
vertices[5] = vertices[0];
_vertexStartIndex += 6;
_curBatchSize += 6;
}
private Span<Vertex2D> AllocateVertexSpace(int count)
{
var space = _vertexBufferPool.Allocate<Vertex2D>(count, out var position);
DebugTools.Assert(
position.ByteOffset % VertexSize == 0,
"Vertices should be getting allocated in exact multiples of vertex size!"
);
if (position.Buffer != _currentVertexBuffer)
{
FlushBatch();
_passEncoder!.SetVertexBuffer(0, position.Buffer);
_currentVertexBuffer = position.Buffer;
_vertexStartIndex = position.ByteOffset / VertexSize;
}
return space;
}
public void SetScissor(int x, int y, int width, int height)
{
checked
{
SetScissor((uint)x, (uint)y, (uint)width, (uint)height);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetScissor(uint x, uint y, uint width, uint height)
{
_stateScissorRect.Set((x, y, width, height), ref _dirtyStateFlags, DirtyStateFlags.ScissorRect);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearScissor()
{
SetScissor(0u, 0u, (uint)_currentPassSize.X, (uint)_currentPassSize.Y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetTexture(ClydeHandle handle)
{
_stateTexture.Set(handle, ref _dirtyStateFlags, DirtyStateFlags.Texture);
}
private void ValidateBatchState()
{
if (_dirtyStateFlags == 0)
return;
DirtyStateFlags sureFlags = 0;
sureFlags |= CheckSureState(ref _stateTexture, DirtyStateFlags.Texture);
sureFlags |= CheckSureState(ref _stateScissorRect, DirtyStateFlags.ScissorRect);
if (sureFlags == 0)
return;
// Something changed. Save the active batch so we can change other rendering parameters.
FlushBatch();
_dirtyStateFlags = 0;
if ((sureFlags & DirtyStateFlags.Texture) != 0)
{
var loaded = _clyde._loadedTextures[_stateTexture.Active];
FlushBatch();
var group = AllocTempBindGroup(new RhiBindGroupDescriptor(
_group2Layout,
new[]
{
new RhiBindGroupEntry(0, loaded.DefaultRhiView),
new RhiBindGroupEntry(1, loaded.RhiSampler),
}
));
_passEncoder!.SetBindGroup(BindGroup2, group);
}
if ((sureFlags & DirtyStateFlags.ScissorRect) != 0)
{
// wgpu doesn't support 0-size scissor rects right now.
// We ignore draws if the scissor state says 0 width.
// TODO: Fix this wgpu-side
if (_stateScissorRect.Active.w != 0 && _stateScissorRect.Active.h != 0)
{
_passEncoder!.SetScissorRect(
_stateScissorRect.Active.x,
_stateScissorRect.Active.y,
_stateScissorRect.Active.w,
_stateScissorRect.Active.h
);
_scissorSkip = false;
}
else
{
_scissorSkip = true;
}
}
DirtyStateFlags CheckSureState<T>(ref State<T> state, DirtyStateFlags thisFlag) where T : IEquatable<T>
{
// Not really sure this is worth checking again overhead-wise.
if ((_dirtyStateFlags & thisFlag) == 0)
return 0;
if (state.Active.Equals(state.New))
return 0;
state.Active = state.New;
return thisFlag;
}
}
private void FlushBatch()
{
if (_curBatchSize == 0)
return;
if (!_scissorSkip)
{
_passEncoder!.Draw(
(uint)_curBatchSize,
1,
(uint)(_vertexStartIndex - _curBatchSize),
0
);
}
_curBatchSize = 0;
}
public void Clear()
{
ClearPassState();
_vertexStartIndex = 0;
_currentVertexBuffer = null;
foreach (var bindGroup in _tempBindGroups)
{
bindGroup.Dispose();
}
_tempBindGroups.Clear();
}
private void ClearPassState()
{
_curBatchSize = 0;
_currentVertexBuffer = null;
_vertexStartIndex = 0;
_dirtyStateFlags = 0;
_stateTexture = default;
_stateScissorRect = default;
_modelTransform = Matrix3x2.Identity;
}
public void SetModelTransform(in Matrix3x2 matrix)
{
_modelTransform = matrix;
}
private RhiBindGroup AllocTempBindGroup(in RhiBindGroupDescriptor descriptor)
{
var bindGroup = _rhi.CreateBindGroup(descriptor);
_tempBindGroups.Add(bindGroup);
return bindGroup;
}
private struct Vertex2D
{
public SVector2 Position;
public SVector2 TexCoord;
public SVector4 Color;
public Vertex2D(SVector2 position, SVector2 texCoord, SVector4 color)
{
Position = position;
TexCoord = texCoord;
Color = color;
}
}
private struct UniformView
{
public ShaderMat3x2F ProjViewMatrix;
public SVector2 ScreenPixelSize;
}
[Flags]
private enum DirtyStateFlags : ushort
{
// @formatter:off
None = 0,
Texture = 1 << 0,
ScissorRect = 1 << 1
// @formatter:on
}
private struct State<T> where T : IEquatable<T>
{
public T Active;
public T New;
public void Set(T newValue, ref DirtyStateFlags flags, DirtyStateFlags thisFlag)
{
if ((flags & thisFlag) != 0)
{
// Property is already dirty, just write it with no further checks.
New = newValue;
return;
}
// Quick case: setting to same value.
if (Active.Equals(newValue))
return;
// Value modified, mark as dirty.
New = newValue;
flags |= thisFlag;
}
}
}
}

View File

@@ -6,7 +6,7 @@ using System.Data;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -15,10 +15,6 @@ using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using PIF = OpenToolkit.Graphics.OpenGL4.PixelInternalFormat;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
@@ -33,7 +29,9 @@ namespace Robust.Client.Graphics.Clyde
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
public OwnedTexture LoadTextureFromPNGStream(
Stream stream,
string? name = null,
TextureLoadParameters? loadParams = null)
{
DebugTools.Assert(_gameThread == Thread.CurrentThread);
@@ -44,82 +42,240 @@ namespace Robust.Client.Graphics.Clyde
return LoadTextureFromImage(image, name, loadParams);
}
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
public OwnedTexture LoadTextureFromImage<T>(
Image<T> image,
string? name = null,
TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>
{
DebugTools.Assert(_gameThread == Thread.CurrentThread);
var actualParams = loadParams ?? TextureLoadParameters.Default;
var pixelType = typeof(T);
if (!_hasGLTextureSwizzle)
{
// If texture swizzle isn't available we have to pre-process the images to apply it ourselves
// and then upload as RGBA8.
// Yes this is inefficient but the alternative is modifying the shaders,
// which I CBA to do.
// Even 8 year old iGPUs support texture swizzle.
if (pixelType == typeof(A8))
{
// Disable sRGB so stuff doesn't get interpreter wrong.
actualParams.Srgb = false;
using var img = ApplyA8Swizzle((Image<A8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
if (pixelType == typeof(L8) && !actualParams.Srgb)
{
using var img = ApplyL8Swizzle((Image<L8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
}
var (instance, loaded) = CreateBlankTextureCore<T>((image.Width, image.Height), name, loadParams);
// Flip image because OpenGL reads images upside down.
using var copy = FlipClone(image);
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
unsafe
{
var span = copy.GetPixelSpan();
fixed (T* ptr = span)
{
// Still bound.
DoTexUpload(copy.Width, copy.Height, actualParams.Srgb, ptr);
}
Rhi.Queue.WriteTexture(
new RhiImageCopyTexture
{
Texture = loaded.RhiTexture,
Aspect = RhiTextureAspect.All,
Origin = new RhiOrigin3D(),
MipLevel = 0
},
MemoryMarshal.Cast<T, byte>(span),
new RhiImageDataLayout(0, (uint) (sizeof(T) * image.Width), (uint) image.Height),
new RhiExtent3D(image.Width, image.Height)
);
}
return texture;
return instance;
}
public unsafe OwnedTexture CreateBlankTexture<T>(
public OwnedTexture CreateBlankTexture<T>(
Vector2i size,
string? name = null,
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>
{
var actualParams = loadParams ?? TextureLoadParameters.Default;
if (!_hasGLTextureSwizzle)
{
// Actually create RGBA32 texture if missing texture swizzle.
// This is fine (TexturePixelType that's stored) because all other APIs do the same.
if (typeof(T) == typeof(A8) || typeof(T) == typeof(L8))
{
return CreateBlankTexture<Rgba32>(size, name, loadParams);
}
}
var texture = CreateBaseTextureInternal<T>(
size.X, size.Y,
actualParams,
name);
// Texture still bound, run glTexImage2D with null data param to specify bounds.
DoTexUpload<T>(size.X, size.Y, actualParams.Srgb, null);
return texture;
var (instance, _) = CreateBlankTextureCore<T>(size, name, loadParams);
return instance;
}
private (ClydeTexture, LoadedTexture) CreateBlankTextureCore<T>(
Vector2i size,
string? name = null,
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>
{
DebugTools.Assert(_gameThread == Thread.CurrentThread);
var actualParams = loadParams ?? TextureLoadParameters.Default;
var srgb = actualParams.Srgb;
var format = GetPixelTextureFormat<T>(srgb);
return CreateBlankTextureCore(size, name, format, actualParams.SampleParameters, srgb);
}
private (ClydeTexture, LoadedTexture) CreateBlankTextureCore(
Vector2i size,
string? name,
RhiTextureFormat format,
TextureSampleParameters sampleParams,
bool srgb)
{
var rhiTexture = Rhi.CreateTexture(new RhiTextureDescriptor(
new RhiExtent3D(size.X, size.Y),
format,
RhiTextureUsage.TextureBinding | RhiTextureUsage.CopySrc | RhiTextureUsage.CopyDst,
Label: name
));
var rhiTextureView = rhiTexture.CreateView(new RhiTextureViewDescriptor
{
Aspect = RhiTextureAspect.All,
Dimension = RhiTextureViewDimension.Dim2D,
Format = format,
Label = name,
MipLevelCount = 1,
ArrayLayerCount = 1,
BaseArrayLayer = 0,
BaseMipLevel = 0
});
var addressMode = sampleParams.WrapMode switch
{
TextureWrapMode.None => RhiAddressMode.ClampToEdge,
TextureWrapMode.Repeat => RhiAddressMode.Repeat,
TextureWrapMode.MirroredRepeat => RhiAddressMode.MirrorRepeat,
_ => throw new ArgumentOutOfRangeException()
};
var filter = sampleParams.Filter ? RhiFilterMode.Linear : RhiFilterMode.Nearest;
// TODO: Cache samplers somewhere, we can't actually make infinite of these and they're simple enough.
var rhiSampler = Rhi.CreateSampler(new RhiSamplerDescriptor(
AddressModeU: addressMode,
AddressModeV: addressMode,
MagFilter: filter,
MinFilter: filter,
Label: name
));
var (width, height) = size;
var id = AllocRid();
var instance = new ClydeTexture(id, size, srgb, this);
var loaded = new LoadedTexture
{
RhiTexture = rhiTexture,
DefaultRhiView = rhiTextureView,
RhiSampler = rhiSampler,
Width = width,
Height = height,
Format = format,
Name = name,
};
_loadedTextures.Add(id, loaded);
return (instance, loaded);
}
private static RhiTextureFormat GetPixelTextureFormat<T>(bool srgb) where T : unmanaged, IPixel<T>
{
return default(T) switch
{
Rgba32 => srgb ? RhiTextureFormat.RGBA8UnormSrgb : RhiTextureFormat.RGBA8Unorm,
_ => throw new ArgumentException("Unsupported pixel format")
};
}
private void TextureSetSubImage<T>(
ClydeTexture clydeTexture,
Vector2i topLeft,
Image<T> sourceImage,
in UIBox2i sourceRegion)
where T : unmanaged, IPixel<T>
{
if (sourceRegion.Left < 0 ||
sourceRegion.Top < 0 ||
sourceRegion.Right > sourceRegion.Width ||
sourceRegion.Bottom > sourceRegion.Height)
{
throw new ArgumentOutOfRangeException(nameof(sourceRegion), "Source rectangle out of bounds.");
}
var size = sourceRegion.Width * sourceRegion.Height;
T[]? pooled = null;
// C# won't let me use an if due to the stackalloc.
var copyBuffer = size < 16 * 16
? stackalloc T[size]
: (pooled = ArrayPool<T>.Shared.Rent(size)).AsSpan(0, size);
var srcSpan = sourceImage.GetPixelSpan();
var w = sourceImage.Width;
FlipCopySubRegion(sourceRegion, w, srcSpan, copyBuffer);
SetSubImageImpl<T>(clydeTexture, topLeft, (sourceRegion.Width, sourceRegion.Height), copyBuffer);
if (pooled != null)
ArrayPool<T>.Shared.Return(pooled);
}
private void TextureSetSubImage<T>(
ClydeTexture clydeTexture,
Vector2i topLeft,
Vector2i size,
ReadOnlySpan<T> buffer)
where T : unmanaged, IPixel<T>
{
T[]? pooled = null;
// C# won't let me use an if due to the stackalloc.
var copyBuffer = buffer.Length < 16 * 16
? stackalloc T[buffer.Length]
: (pooled = ArrayPool<T>.Shared.Rent(buffer.Length)).AsSpan(0, buffer.Length);
FlipCopy(buffer, copyBuffer, size.X, size.Y);
SetSubImageImpl<T>(clydeTexture, topLeft, size, copyBuffer);
if (pooled != null)
ArrayPool<T>.Shared.Return(pooled);
}
private unsafe void SetSubImageImpl<T>(
ClydeTexture texture,
Vector2i dstTl,
Vector2i size,
ReadOnlySpan<T> buf)
where T : unmanaged, IPixel<T>
{
var loaded = _loadedTextures[texture.TextureId];
var format = GetPixelTextureFormat<T>(loaded.IsSrgb);
if (format != loaded.Format)
{
// TODO:
//if (loaded.TexturePixelType == TexturePixelType.RenderTarget)
// throw new InvalidOperationException("Cannot modify texture for render target directly.");
throw new InvalidOperationException("Mismatching pixel type for texture.");
}
if (loaded.Width < dstTl.X + size.X || loaded.Height < dstTl.Y + size.Y)
throw new ArgumentOutOfRangeException(nameof(size), "Destination rectangle out of bounds.");
var dstY = loaded.Height - dstTl.Y - size.Y;
Rhi.Queue.WriteTexture(
new RhiImageCopyTexture(
loaded.RhiTexture,
0,
new RhiOrigin3D(dstTl.X, dstY)
),
buf,
new RhiImageDataLayout(0, (uint) (size.X * sizeof(T)), (uint) size.Y),
new RhiExtent3D(size.X, size.Y)
);
// GL.TexSubImage2D(
// TextureTarget.Texture2D,
// 0,
// dstTl.X, dstY,
// size.X, size.Y,
// pf, pt,
// (IntPtr) aPtr);
// CheckGlError();
}
/*
private unsafe void DoTexUpload<T>(int width, int height, bool srgb, T* ptr) where T : unmanaged, IPixel<T>
{
if (sizeof(T) < 4)
@@ -256,7 +412,9 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
*/
/*
private (PIF pif, PF pf, PT pt) PixelEnums<T>(bool srgb)
where T : unmanaged, IPixel<T>
{
@@ -279,7 +437,9 @@ namespace Robust.Client.Graphics.Clyde
_ => throw new NotSupportedException("Unsupported pixel type."),
};
}
*/
/*
private ClydeTexture GenTexture(
GLHandle glHandle,
Vector2i size,
@@ -314,9 +474,11 @@ namespace Robust.Client.Graphics.Clyde
return instance;
}
*/
private void DeleteTexture(ClydeHandle textureHandle)
{
/*
if (!_loadedTextures.TryGetValue(textureHandle, out var loadedTexture))
{
// Already deleted I guess.
@@ -327,154 +489,22 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
_loadedTextures.Remove(textureHandle);
//GC.RemoveMemoryPressure(loadedTexture.MemoryPressure);
}
private unsafe void SetSubImage<T>(
ClydeTexture texture,
Vector2i dstTl,
Image<T> img,
in UIBox2i srcBox)
where T : unmanaged, IPixel<T>
{
if (srcBox.Left < 0 ||
srcBox.Top < 0 ||
srcBox.Right > srcBox.Width ||
srcBox.Bottom > srcBox.Height)
{
throw new ArgumentOutOfRangeException(nameof(srcBox), "Source rectangle out of bounds.");
}
var size = srcBox.Width * srcBox.Height;
T[]? pooled = null;
// C# won't let me use an if due to the stackalloc.
var copyBuffer = size < 16 * 16
? stackalloc T[size]
: (pooled = ArrayPool<T>.Shared.Rent(size)).AsSpan(0, size);
var srcSpan = img.GetPixelSpan();
var w = img.Width;
FlipCopySubRegion(srcBox, w, srcSpan, copyBuffer);
SetSubImageImpl<T>(texture, dstTl, (srcBox.Width, srcBox.Height), copyBuffer);
if (pooled != null)
ArrayPool<T>.Shared.Return(pooled);
}
private unsafe void SetSubImage<T>(
ClydeTexture texture,
Vector2i dstTl,
Vector2i size,
ReadOnlySpan<T> buf)
where T : unmanaged, IPixel<T>
{
T[]? pooled = null;
// C# won't let me use an if due to the stackalloc.
var copyBuffer = buf.Length < 16 * 16
? stackalloc T[buf.Length]
: (pooled = ArrayPool<T>.Shared.Rent(buf.Length)).AsSpan(0, buf.Length);
FlipCopy(buf, copyBuffer, size.X, size.Y);
SetSubImageImpl<T>(texture, dstTl, size, copyBuffer);
if (pooled != null)
ArrayPool<T>.Shared.Return(pooled);
}
private unsafe void SetSubImageImpl<T>(
ClydeTexture texture,
Vector2i dstTl,
Vector2i size,
ReadOnlySpan<T> buf)
where T : unmanaged, IPixel<T>
{
if (!_hasGLTextureSwizzle && (typeof(T) == typeof(A8) || typeof(T) == typeof(L8)))
{
var swizzleBuf = ArrayPool<Rgba32>.Shared.Rent(buf.Length);
var destSpan = swizzleBuf.AsSpan(0, buf.Length);
if (typeof(T) == typeof(A8))
ApplyA8Swizzle(MemoryMarshal.Cast<T, A8>(buf), destSpan);
else if (typeof(T) == typeof(L8))
ApplyL8Swizzle(MemoryMarshal.Cast<T, L8>(buf), destSpan);
SetSubImageImpl<Rgba32>(texture, dstTl, size, destSpan);
ArrayPool<Rgba32>.Shared.Return(swizzleBuf);
return;
}
var loaded = _loadedTextures[texture.TextureId];
var pixType = GetTexturePixelType<T>();
if (pixType != loaded.TexturePixelType)
{
if (loaded.TexturePixelType == TexturePixelType.RenderTarget)
throw new InvalidOperationException("Cannot modify texture for render target directly.");
throw new InvalidOperationException("Mismatching pixel type for texture.");
}
if (loaded.Width < dstTl.X + size.X || loaded.Height < dstTl.Y + size.Y)
throw new ArgumentOutOfRangeException(nameof(size), "Destination rectangle out of bounds.");
if (sizeof(T) != 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
CheckGlError();
}
// sRGB doesn't matter since that only changes the internalFormat, which we don't need here.
var (_, pf, pt) = PixelEnums<T>(srgb: false);
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
CheckGlError();
fixed (T* aPtr = buf)
{
var dstY = loaded.Height - dstTl.Y - size.Y;
GL.TexSubImage2D(
TextureTarget.Texture2D,
0,
dstTl.X, dstY,
size.X, size.Y,
pf, pt,
(IntPtr) aPtr);
CheckGlError();
}
if (sizeof(T) != 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
CheckGlError();
}
}
private static TexturePixelType GetTexturePixelType<T>() where T : unmanaged, IPixel<T>
{
return default(T) switch
{
Rgba32 => TexturePixelType.Rgba32,
L8 => TexturePixelType.L8,
A8 => TexturePixelType.A8,
_ => throw new NotSupportedException("Unsupported pixel type."),
};
*/
}
private void LoadStockTextures()
{
var white = new Image<Rgba32>(1, 1);
white[0, 0] = new Rgba32(255, 255, 255, 255);
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white, name: "StockTextureWhite");
_stockTextureWhite = (ClydeTexture)LoadTextureFromImage(white, name: "Stock Texture: white");
var black = new Image<Rgba32>(1, 1);
black[0, 0] = new Rgba32(0, 0, 0, 255);
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black, name: "StockTextureBlack");
_stockTextureBlack = (ClydeTexture)LoadTextureFromImage(black, name: "Stock Texture: black");
var blank = new Image<Rgba32>(1, 1);
blank[0, 0] = new Rgba32(0, 0, 0, 0);
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank, name: "StockTextureTransparent");
_stockTextureTransparent = (ClydeTexture)LoadTextureFromImage(blank, name: "Stock Texture: transparent");
}
/// <summary>
@@ -573,32 +603,27 @@ namespace Robust.Client.Graphics.Clyde
internal sealed class LoadedTexture
{
public GLHandle OpenGLObject;
public RhiTexture RhiTexture = default!;
public RhiTextureView DefaultRhiView = default!;
public RhiSampler RhiSampler = default!;
public int Width;
public int Height;
public bool IsSrgb;
public RhiTextureFormat Format;
public string? Name;
public long MemoryPressure;
public TexturePixelType TexturePixelType;
public Vector2i Size => (Width, Height);
public required WeakReference<ClydeTexture> TextureInstance;
}
internal enum TexturePixelType : byte
{
RenderTarget = 0,
Rgba32,
A8,
L8,
public bool IsSrgb => Format is RhiTextureFormat.BGRA8UnormSrgb or RhiTextureFormat.RGBA8UnormSrgb;
}
private void FlushTextureDispose()
{
while (_textureDisposeQueue.TryDequeue(out var handle))
/*while (_textureDisposeQueue.TryDequeue(out var handle))
{
DeleteTexture(handle);
}
}*/
}
internal sealed class ClydeTexture : OwnedTexture
@@ -610,17 +635,17 @@ namespace Robust.Client.Graphics.Clyde
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
{
_clyde.SetSubImage(this, topLeft, sourceImage, sourceRegion);
_clyde.TextureSetSubImage(this, topLeft, sourceImage, sourceRegion);
}
public override void SetSubImage<T>(Vector2i topLeft, Vector2i size, ReadOnlySpan<T> buffer)
{
_clyde.SetSubImage(this, topLeft, size, buffer);
_clyde.TextureSetSubImage(this, topLeft, size, buffer);
}
protected override void Dispose(bool disposing)
{
if (_clyde.IsMainThread())
/*if (_clyde.IsMainThread())
{
// Main thread, do direct GL deletion.
_clyde.DeleteTexture(TextureId);
@@ -629,7 +654,7 @@ namespace Robust.Client.Graphics.Clyde
{
// Finalizer thread
_clyde._textureDisposeQueue.Enqueue(TextureId);
}
}*/
}
internal ClydeTexture(ClydeHandle id, Vector2i size, bool srgb, Clyde clyde) : base(size)
@@ -648,32 +673,6 @@ namespace Robust.Client.Graphics.Clyde
return $"ClydeTexture: ({TextureId})";
}
public override unsafe Color GetPixel(int x, int y)
{
if (!_clyde._loadedTextures.TryGetValue(TextureId, out var loaded))
{
throw new DataException("Texture not found");
}
var curTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
var bufSize = 4 * loaded.Size.X * loaded.Size.Y;
var buffer = ArrayPool<byte>.Shared.Rent(bufSize);
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
fixed (byte* p = buffer)
{
GL.GetTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, (IntPtr) p);
}
GL.BindTexture(TextureTarget.Texture2D, curTexture2D);
var pixelPos = (loaded.Size.X * (loaded.Size.Y - y - 1) + x) * 4;
var color = new Color(buffer[pixelPos+0], buffer[pixelPos+1], buffer[pixelPos+2], buffer[pixelPos+3]);
ArrayPool<byte>.Shared.Return(buffer);
return color;
}
}
public Texture GetStockTexture(ClydeStockTexture stockTexture)
@@ -697,5 +696,20 @@ namespace Robust.Client.Graphics.Clyde
yield return (textureInstance, loaded);
}
}
private sealed class DummyTexture : OwnedTexture
{
public DummyTexture(Vector2i size) : base(size)
{
}
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
{
}
public override void SetSubImage<T>(Vector2i topLeft, Vector2i size, ReadOnlySpan<T> buffer)
{
}
}
}
}

View File

@@ -32,7 +32,7 @@ namespace Robust.Client.Graphics.Clyde
name: $"{name}-MainRenderTarget")
};
RegenLightRts(viewport);
// RegenLightRts(viewport);
_viewports.Add(handle, new WeakReference<Viewport>(viewport));
@@ -143,7 +143,7 @@ namespace Robust.Client.Graphics.Clyde
void IClydeViewport.Render()
{
_clyde.RenderViewport(this);
// _clyde.RenderViewport(this);
}
public MapCoordinates LocalToWorld(Vector2 point)
@@ -198,7 +198,7 @@ namespace Robust.Client.Graphics.Clyde
IViewportControl control,
in UIBox2i viewportBounds)
{
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpaceBelowWorld, viewportBounds);
// _clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpaceBelowWorld, viewportBounds);
}
public void RenderScreenOverlaysAbove(
@@ -206,7 +206,7 @@ namespace Robust.Client.Graphics.Clyde
IViewportControl control,
in UIBox2i viewportBounds)
{
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
// _clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
}
~Viewport()
@@ -217,12 +217,12 @@ namespace Robust.Client.Graphics.Clyde
public void Dispose()
{
GC.SuppressFinalize(this);
// TODO: These shouldn't be ?. disposes. Temporary thing for WebGPU.
RenderTarget.Dispose();
LightRenderTarget.Dispose();
WallMaskRenderTarget.Dispose();
WallBleedIntermediateRenderTarget1.Dispose();
WallBleedIntermediateRenderTarget2.Dispose();
LightRenderTarget?.Dispose();
WallMaskRenderTarget?.Dispose();
WallBleedIntermediateRenderTarget1?.Dispose();
WallBleedIntermediateRenderTarget2?.Dispose();
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
}

View File

@@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared;
@@ -16,7 +17,6 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
using FrameEventArgs = Robust.Shared.Timing.FrameEventArgs;
using GL = OpenToolkit.Graphics.OpenGL4.GL;
namespace Robust.Client.Graphics.Clyde
{
@@ -27,21 +27,15 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<int, MonitorHandle> _monitorHandles = new();
private int _primaryMonitorId;
private WindowReg? _mainWindow;
internal WindowReg? _mainWindow;
private IWindowingImpl? _windowing;
#pragma warning disable 414
// Keeping this for if/when we ever get a new renderer.
private Renderer _chosenRenderer;
#pragma warning restore 414
internal IWindowingImpl? _windowing;
private ResPath? _windowIconPath;
private Thread? _windowingThread;
private bool _vSync;
private WindowMode _windowMode;
private WindowReg? _currentHoveredWindow;
private bool _threadWindowBlit;
private bool EffectiveThreadWindowBlit => _threadWindowBlit && !_isGLES;
public event Action<TextEnteredEventArgs>? TextEntered;
public event Action<TextEditingEventArgs>? TextEditing;
@@ -91,11 +85,6 @@ namespace Robust.Client.Graphics.Clyde
private bool InitWindowing()
{
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngleEs3On10_0))
{
Environment.SetEnvironmentVariable("ANGLE_FEATURE_OVERRIDES_ENABLED", "allowES3OnFL10_0");
}
var iconPath = _cfg.GetCVar(CVars.DisplayWindowIconSet);
if (!string.IsNullOrWhiteSpace(iconPath))
_windowIconPath = new ResPath(iconPath);
@@ -127,10 +116,8 @@ namespace Robust.Client.Graphics.Clyde
return _windowing.Init();
}
private bool TryInitMainWindow(GLContextSpec? glSpec, [NotNullWhen(false)] out string? error)
private bool TryInitMainWindow([NotNullWhen(false)] out string? error)
{
DebugTools.AssertNotNull(_glContext);
var width = _cfg.GetCVar(CVars.DisplayWidth);
var height = _cfg.GetCVar(CVars.DisplayHeight);
var prevWidth = width;
@@ -155,7 +142,7 @@ namespace Robust.Client.Graphics.Clyde
Fullscreen = fullscreen
};
var (reg, err) = SharedWindowCreate(glSpec, parameters, null, isMain: true);
var (reg, err) = SharedWindowCreate(parameters, isMain: true);
if (reg == null)
{
@@ -178,43 +165,18 @@ namespace Robust.Client.Graphics.Clyde
private unsafe bool InitMainWindowAndRenderer()
{
DebugTools.AssertNotNull(_windowing);
DebugTools.AssertNotNull(_glContext);
_chosenRenderer = Renderer.OpenGL;
var succeeded = false;
string? lastError = null;
if (_glContext!.RequireWindowGL)
{
var specs = _glContext!.SpecsToTry;
foreach (var glSpec in specs)
{
if (!TryInitMainWindow(glSpec, out lastError))
{
_sawmillWin.Debug($"OpenGL {glSpec.OpenGLVersion} unsupported: {lastError}");
continue;
}
succeeded = true;
break;
}
}
if (!TryInitMainWindow(out lastError))
_sawmillWin.Debug($"Failed to create window: {lastError}");
else
{
if (!TryInitMainWindow(null, out lastError))
_sawmillWin.Debug($"Failed to create window: {lastError}");
else
succeeded = true;
}
succeeded = true;
// We should have a main window now.
DebugTools.AssertNotNull(_mainWindow);
// _openGLVersion must be set by _glContext.
DebugTools.Assert(_openGLVersion != RendererOpenGLVersion.Auto);
if (!succeeded)
{
if (OperatingSystem.IsWindows())
@@ -242,15 +204,13 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
if (!_earlyGLInit)
InitOpenGL();
InitRhi();
_sawmillOgl.Debug("Setting viewport and rendering splash...");
GL.Viewport(0, 0, ScreenSize.X, ScreenSize.Y);
CheckGlError();
_debugInfo = new ClydeDebugInfo(_windowing!.GetDescription());
// Quickly do a render with _drawingSplash = true so the screen isn't blank.
RenderInit();
Render();
return true;
@@ -308,17 +268,10 @@ namespace Robust.Client.Graphics.Clyde
public IClydeWindow CreateWindow(WindowCreateParameters parameters)
{
DebugTools.AssertNotNull(_windowing);
DebugTools.AssertNotNull(_glContext);
DebugTools.AssertNotNull(_mainWindow);
var glSpec = _glContext!.GetNewWindowSpec();
_glContext.BeforeSharedWindowCreateUnbind();
var (reg, error) = SharedWindowCreate(
glSpec,
parameters,
glSpec == null ? null : _mainWindow,
isMain: false);
// Rebinding is handed by WindowCreated in the GL context.
@@ -329,17 +282,13 @@ namespace Robust.Client.Graphics.Clyde
return reg!.Handle;
}
private (WindowReg?, string? error) SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
WindowReg? share,
bool isMain)
private (WindowReg?, string? error) SharedWindowCreate(WindowCreateParameters parameters, bool isMain)
{
WindowReg? owner = null;
if (parameters.Owner != null)
owner = ((WindowHandle)parameters.Owner).Reg;
var (reg, error) = _windowing!.WindowCreate(glSpec, parameters, share, owner);
var (reg, error) = _windowing!.WindowCreate(parameters, owner);
if (reg != null)
{
@@ -366,7 +315,8 @@ namespace Robust.Client.Graphics.Clyde
reg.RenderTarget = renderTarget;
_glContext!.WindowCreated(glSpec, reg);
if (!isMain)
Rhi.WindowCreated(reg);
}
// Pass through result whether successful or not, caller handles it.
@@ -385,7 +335,8 @@ namespace Robust.Client.Graphics.Clyde
reg.IsDisposed = true;
_glContext!.WindowDestroyed(reg);
Rhi.WindowDestroy(reg);
_windowing!.WindowDestroy(reg);
_windows.Remove(reg);
@@ -402,22 +353,10 @@ namespace Robust.Client.Graphics.Clyde
DispatchEvents();
}
private void SwapAllBuffers()
{
_glContext?.SwapAllBuffers();
}
public bool VsyncEnabled
{
get => _vSync;
set
{
if (_vSync == value)
return;
_vSync = value;
_glContext?.UpdateVSync();
}
set => _vSync = value;
}
private void WindowModeChanged(int mode)
@@ -485,7 +424,7 @@ namespace Robust.Client.Graphics.Clyde
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
private abstract class WindowReg
internal abstract class WindowReg
{
public bool IsDisposed;
@@ -504,6 +443,9 @@ namespace Robust.Client.Graphics.Clyde
public bool IsVisible;
public IClydeWindow? Owner;
public RhiWebGpu.WindowData? RhiWebGpuData;
public RhiTextureView? CurSwapchainView;
public bool DisposeOnClose;
public bool IsMainWindow;
@@ -514,7 +456,7 @@ namespace Robust.Client.Graphics.Clyde
public Action<WindowResizedEventArgs>? Resized;
}
private sealed class WindowHandle : IClydeWindowInternal
internal sealed class WindowHandle : IClydeWindowInternal
{
// So funny story
// When this class was a record, the C# compiler on .NET 5 stack overflowed

View File

@@ -1,10 +1,6 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Map;
@@ -24,9 +20,6 @@ using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
{
/// <summary>
@@ -34,6 +27,7 @@ namespace Robust.Client.Graphics.Clyde
/// </summary>
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEventSubscriber
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
@@ -53,56 +47,25 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IReloadManager _reloads = default!;
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
private GLBuffer BatchVBO = default!;
private GLBuffer BatchEBO = default!;
private GLHandle BatchVAO;
// VBO to draw a single quad.
private GLBuffer QuadVBO = default!;
private GLHandle QuadVAO;
// VBO to blit to the window
// VAO is per-window and not stored (not necessary!)
private GLBuffer WindowVBO = default!;
private bool _drawingSplash = true;
private GLShaderProgram? _currentProgram;
private float _lightResolutionScale = 0.5f;
private int _maxLights = 2048;
private int _maxOccluders = 2048;
private int _maxShadowcastingLights = 128;
private bool _enableSoftShadows = true;
private bool _checkGLErrors;
private Thread? _gameThread;
private ISawmill _clydeSawmill = default!;
private ISawmill _sawmillOgl = default!;
private ISawmill _sawmillWin = default!;
private IBindingsContext _glBindingsContext = default!;
private bool _earlyGLInit;
private bool _threadWindowApi;
public Clyde()
{
_currentBoundRenderTarget = default!;
_currentRenderTarget = default!;
SixLabors.ImageSharp.Configuration.Default.PreferContiguousImageBuffers = true;
}
public bool InitializePreWindowing()
{
_clydeSawmill = _logManager.GetSawmill("clyde");
_sawmillOgl = _logManager.GetSawmill("clyde.ogl");
_sawmillWin = _logManager.GetSawmill("clyde.win");
_reloads.Register("/Shaders", "*.swsl");
_reloads.Register("/Textures/Shaders", "*.swsl");
_reloads.Register("/Textures", "*.jpg");
@@ -113,26 +76,24 @@ namespace Robust.Client.Graphics.Clyde
_reloads.OnChanged += OnChange;
_proto.PrototypesReloaded += OnProtoReload;
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
/*
_cfg.OnValueChanged(CVars.LightResolutionScale, LightResolutionScaleChanged, true);
_cfg.OnValueChanged(CVars.MaxShadowcastingLights, MaxShadowcastingLightsChanged, true);
_cfg.OnValueChanged(CVars.LightSoftShadows, SoftShadowsChanged, true);
_cfg.OnValueChanged(CVars.MaxLightCount, MaxLightsChanged, true);
_cfg.OnValueChanged(CVars.MaxOccluderCount, MaxOccludersChanged, true);
_cfg.OnValueChanged(CVars.RenderTileEdges, RenderTileEdgesChanges, true);
*/
// I can't be bothered to tear down and set these threads up in a cvar change handler.
// Windows and Linux can be trusted to not explode with threaded windowing,
// macOS cannot.
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
_cfg.OverrideDefault(CVars.DisplayThreadWindowApi, true);
#if MACOS
// Trust macOS to not need threaded window blitting.
// (threaded window blitting is a workaround to avoid having to frequently MakeCurrent() on Windows, as it is broken).
_cfg.OverrideDefault(CVars.DisplayThreadWindowBlit, false);
#endif
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
_threadWindowApi = _cfg.GetCVar(CVars.DisplayThreadWindowApi);
InitKeys();
@@ -178,7 +139,6 @@ namespace Robust.Client.Graphics.Clyde
InitSystems();
InitGLContextManager();
if (!InitMainWindowAndRenderer())
return false;
@@ -206,7 +166,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing?.FlushDispose();
FlushShaderInstanceDispose();
FlushRenderTargetDispose();
// FlushRenderTargetDispose();
FlushTextureDispose();
FlushViewportDispose();
}
@@ -215,17 +175,17 @@ namespace Robust.Client.Graphics.Clyde
{
_drawingSplash = false;
InitLighting();
// InitLighting();
}
public IClydeDebugInfo DebugInfo { get; private set; } = default!;
public IClydeDebugInfo DebugInfo => _debugInfo ?? throw new InvalidOperationException("Not initialized yet");
public IClydeDebugStats DebugStats => _debugStats;
public void PostInject()
{
// This cvar does not modify the actual GL version requested or anything,
// it overrides the version we detect to detect GL features.
RegisterBlockCVars();
// RegisterBlockCVars();
}
public void RegisterGridEcsEvents()
@@ -242,362 +202,8 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.UnsubscribeEvent<GridRemovalEvent>(EventSource.Local, this);
}
private void GLInitBindings(bool gles)
{
_glBindingsContext = _glContext!.BindingsContext;
GL.LoadBindings(_glBindingsContext);
if (gles)
{
// On GLES we use some OES and KHR functions so make sure to initialize them.
OpenToolkit.Graphics.ES20.GL.LoadBindings(_glBindingsContext);
}
}
private void InitOpenGL()
{
_isGLES = _openGLVersion is RendererOpenGLVersion.GLES2 or RendererOpenGLVersion.GLES3;
_isGLES2 = _openGLVersion is RendererOpenGLVersion.GLES2;
_isCore = _openGLVersion is RendererOpenGLVersion.GL33;
GLInitBindings(_isGLES);
var vendor = GL.GetString(StringName.Vendor);
var renderer = GL.GetString(StringName.Renderer);
var version = GL.GetString(StringName.Version);
// GLES2 doesn't allow you to query major/minor version. Seriously.
var major = _openGLVersion == RendererOpenGLVersion.GLES2 ? 2 : GL.GetInteger(GetPName.MajorVersion);
var minor = _openGLVersion == RendererOpenGLVersion.GLES2 ? 0 :GL.GetInteger(GetPName.MinorVersion);
_sawmillOgl.Debug("OpenGL Vendor: {0}", vendor);
_sawmillOgl.Debug("OpenGL Renderer: {0}", renderer);
_sawmillOgl.Debug("OpenGL Version: {0}", version);
var overrideVersion = ParseGLOverrideVersion();
if (overrideVersion != null)
{
(major, minor) = overrideVersion.Value;
_sawmillOgl.Debug("OVERRIDING detected GL version to: {0}.{1}", major, minor);
}
DetectOpenGLFeatures(major, minor);
SetupDebugCallback();
LoadVendorSettings(vendor, renderer, version);
var glVersion = new OpenGLVersion((byte) major, (byte) minor, _isGLES, _isCore);
DebugInfo = new ClydeDebugInfo(
glVersion,
renderer,
vendor,
version,
overrideVersion != null,
_windowing!.GetDescription());
IsBlending = true;
if (_hasGLSrgb && !_isGLES)
{
GL.Enable(EnableCap.FramebufferSrgb);
CheckGlError();
}
if (_hasGLPrimitiveRestart)
{
GL.Enable(EnableCap.PrimitiveRestart);
CheckGlError();
GL.PrimitiveRestartIndex(PrimitiveRestartIndex);
CheckGlError();
}
if (_hasGLPrimitiveRestartFixedIndex)
{
GL.Enable(EnableCap.PrimitiveRestartFixedIndex);
CheckGlError();
}
if (!HasGLAnyVertexArrayObjects)
{
_sawmillOgl.Warning("NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");
}
ResetBlendFunc();
CheckGlError();
// Primitive Restart's presence or lack thereof changes the amount of required memory.
InitRenderingBatchBuffers();
_sawmillOgl.Debug("Loading stock textures...");
LoadStockTextures();
_sawmillOgl.Debug("Loading stock shaders...");
LoadStockShaders();
_sawmillOgl.Debug("Creating various GL objects...");
CreateMiscGLObjects();
_sawmillOgl.Debug("Setting up RenderHandle...");
_renderHandle = new RenderHandle(this, _entityManager);
}
private (int major, int minor)? ParseGLOverrideVersion()
{
var overrideGLVersion = _cfg.GetCVar(CVars.DisplayOGLOverrideVersion);
if (string.IsNullOrEmpty(overrideGLVersion))
{
return null;
}
var split = overrideGLVersion.Split(".");
if (split.Length != 2)
{
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
return null;
}
if (!int.TryParse(split[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var major)
|| !int.TryParse(split[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var minor))
{
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
return null;
}
return (major, minor);
}
private unsafe void CreateMiscGLObjects()
{
// Quad drawing.
{
Span<Vertex2D> quadVertices = stackalloc[]
{
new Vertex2D(1, 0, 1, 1, Color.White),
new Vertex2D(0, 0, 0, 1, Color.White),
new Vertex2D(1, 1, 1, 0, Color.White),
new Vertex2D(0, 1, 0, 0, Color.White)
};
QuadVBO = new GLBuffer<Vertex2D>(this, BufferTarget.ArrayBuffer, BufferUsageHint.StaticDraw,
quadVertices,
nameof(QuadVBO));
QuadVAO = MakeQuadVao();
CheckGlError();
}
// Window VBO
{
Span<Vertex2D> winVertices = stackalloc[]
{
new Vertex2D(-1, 1, 0, 1, Color.White),
new Vertex2D(-1, -1, 0, 0, Color.White),
new Vertex2D(1, 1, 1, 1, Color.White),
new Vertex2D(1, -1, 1, 0, Color.White),
};
WindowVBO = new GLBuffer<Vertex2D>(
this,
BufferTarget.ArrayBuffer,
BufferUsageHint.StaticDraw,
winVertices,
nameof(WindowVBO));
CheckGlError();
}
// Batch rendering
{
BatchVBO = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
sizeof(Vertex2D) * BatchVertexData.Length, nameof(BatchVBO));
BatchVAO = new GLHandle(GenVertexArray());
BindVertexArray(BatchVAO.Handle);
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, BatchVAO, nameof(BatchVAO));
SetupVAOLayout();
CheckGlError();
BatchEBO = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
sizeof(ushort) * BatchIndexData.Length, nameof(BatchEBO));
}
ProjViewUBO = new GLUniformBuffer<ProjViewMatrices>(this, BindingIndexProjView, nameof(ProjViewUBO));
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
screenBufferHandle = new GLHandle(GL.GenTexture());
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
ApplySampleParameters(new TextureSampleParameters() { Filter = false, WrapMode = TextureWrapMode.MirroredRepeat});
// TODO: This is atrocious and broken and awful why did I merge this
ScreenBufferTexture = GenTexture(screenBufferHandle, (1920, 1080), true, null, TexturePixelType.Rgba32);
}
private GLHandle MakeQuadVao()
{
var vao = new GLHandle(GenVertexArray());
BindVertexArray(vao.Handle);
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, nameof(QuadVAO));
GL.BindBuffer(BufferTarget.ArrayBuffer, QuadVBO.ObjectHandle);
SetupVAOLayout();
return vao;
}
[Conditional("DEBUG")]
private unsafe void SetupDebugCallback()
{
if (!_hasGLKhrDebug)
{
_sawmillOgl.Debug("KHR_debug not present, OpenGL debug logging not enabled.");
return;
}
GL.Enable(EnableCap.DebugOutput);
GL.Enable(EnableCap.DebugOutputSynchronous);
_debugMessageCallbackInstance ??= DebugMessageCallback;
// OpenTK seemed to have trouble marshalling the delegate so do it manually.
var procName = _isGLKhrDebugESExtension ? "glDebugMessageCallbackKHR" : "glDebugMessageCallback";
var glDebugMessageCallback = (delegate* unmanaged[Stdcall] <nint, nint, void>) LoadGLProc(procName);
var funcPtr = Marshal.GetFunctionPointerForDelegate(_debugMessageCallbackInstance);
glDebugMessageCallback(funcPtr, new IntPtr(0x3005));
}
private void DebugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity,
int length, IntPtr message, IntPtr userParam)
{
var contents = $"{source}: " + Marshal.PtrToStringAnsi(message, length);
var category = "ogl.debug";
switch (type)
{
case DebugType.DebugTypePerformance:
category += ".performance";
break;
case DebugType.DebugTypeOther:
category += ".other";
break;
case DebugType.DebugTypeError:
category += ".error";
break;
case DebugType.DebugTypeDeprecatedBehavior:
category += ".deprecated";
break;
case DebugType.DebugTypeUndefinedBehavior:
category += ".ub";
break;
case DebugType.DebugTypePortability:
category += ".portability";
break;
case DebugType.DebugTypeMarker:
case DebugType.DebugTypePushGroup:
case DebugType.DebugTypePopGroup:
// These are inserted by our own code so I imagine they're not necessary to log?
return;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
var sawmill = _logManager.GetSawmill(category);
switch (severity)
{
case DebugSeverity.DontCare:
sawmill.Info(contents);
break;
case DebugSeverity.DebugSeverityNotification:
sawmill.Info(contents);
break;
case DebugSeverity.DebugSeverityHigh:
sawmill.Error(contents);
break;
case DebugSeverity.DebugSeverityMedium:
sawmill.Error(contents);
break;
case DebugSeverity.DebugSeverityLow:
sawmill.Warning(contents);
break;
default:
throw new ArgumentOutOfRangeException(nameof(severity), severity, null);
}
}
private static DebugProc? _debugMessageCallbackInstance;
[Conditional("DEBUG")]
private void ObjectLabelMaybe(ObjectLabelIdentifier identifier, uint name, string? label)
{
if (label == null)
{
return;
}
if (!_hasGLKhrDebug || !_glDebuggerPresent)
return;
if (_isGLKhrDebugESExtension)
{
GL.Khr.ObjectLabel((ObjectIdentifier) identifier, name, label.Length, label);
}
else
{
GL.ObjectLabel(identifier, name, label.Length, label);
}
}
[Conditional("DEBUG")]
private void ObjectLabelMaybe(ObjectLabelIdentifier identifier, GLHandle name, string? label)
{
ObjectLabelMaybe(identifier, name.Handle, label);
}
private PopDebugGroup DebugGroup(string group)
{
PushDebugGroupMaybe(group);
return new PopDebugGroup(this);
}
[Conditional("DEBUG")]
private void PushDebugGroupMaybe(string group)
{
// ANGLE spams console log messages when using debug groups, so let's only use them if we're debugging GL.
if (!_hasGLKhrDebug || !_glDebuggerPresent)
return;
if (_isGLKhrDebugESExtension)
{
GL.Khr.PushDebugGroup((DebugSource) DebugSourceExternal.DebugSourceApplication, 0, group.Length, group);
}
else
{
GL.PushDebugGroup(DebugSourceExternal.DebugSourceApplication, 0, group.Length, group);
}
}
[Conditional("DEBUG")]
private void PopDebugGroupMaybe()
{
if (!_hasGLKhrDebug || !_glDebuggerPresent)
return;
if (_isGLKhrDebugESExtension)
{
GL.Khr.PopDebugGroup();
}
else
{
GL.PopDebugGroup();
}
}
public void Shutdown()
{
_glContext?.Shutdown();
ShutdownWindowing();
}

View File

@@ -5,6 +5,7 @@ using System.Numerics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@@ -26,6 +27,8 @@ namespace Robust.Client.Graphics.Clyde
[UsedImplicitly]
internal sealed class ClydeHeadless : IClydeInternal
{
public RhiBase Rhi => throw new NotImplementedException();
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
public IClydeWindow MainWindow { get; }
public Vector2i ScreenSize => (1280, 720);
@@ -349,11 +352,6 @@ namespace Robust.Client.Graphics.Clyde
{
// Just do nothing on mutate.
}
public override Color GetPixel(int x, int y)
{
return Color.Black;
}
}
private sealed class DummyShaderInstance : ShaderInstance
@@ -491,11 +489,6 @@ namespace Robust.Client.Graphics.Clyde
private sealed class DummyDebugInfo : IClydeDebugInfo
{
public OpenGLVersion OpenGLVersion { get; } = new(3, 3, isES: false, isCore: true);
public string Renderer => "ClydeHeadless";
public string Vendor => "Space Wizards Federation";
public string VersionString { get; } = $"3.3.0 WIZARDS {typeof(DummyDebugInfo).Assembly.GetName().Version}";
public bool Overriding => false;
public string WindowingApi => "The vast abyss";
}

View File

@@ -1,582 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static Robust.Client.Graphics.Clyde.Egl;
using static TerraFX.Interop.DirectX.D3D_DRIVER_TYPE;
using static TerraFX.Interop.DirectX.D3D_FEATURE_LEVEL;
using static TerraFX.Interop.DirectX.DXGI_FORMAT;
using static TerraFX.Interop.DirectX.DXGI_SWAP_EFFECT;
using static TerraFX.Interop.Windows.Windows;
using static TerraFX.Interop.DirectX.DirectX;
using static TerraFX.Interop.DirectX.D3D11;
using static TerraFX.Interop.DirectX.DXGI;
using GL = OpenToolkit.Graphics.OpenGL4.GL;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Explicit ANGLE GL context with manual DXGI/D3D device and swap chain management.
/// </summary>
private sealed unsafe class GLContextAngle : GLContextBase
{
// Thanks to mpv's implementation of context_angle for inspiration/hints.
// https://github.com/mpv-player/mpv/blob/f8e62d3d82dd0a3d06f9a557d756f0ad78118cc7/video/out/opengl/context_angle.c
// NOTE: This class only handles GLES3/D3D11.
// For anything lower we just let ANGLE fall back and do the work 100%.
private readonly ISawmill _sawmill;
private IDXGIFactory1* _factory;
private IDXGIAdapter1* _adapter;
private ID3D11Device* _device;
private D3D_FEATURE_LEVEL _deviceFl;
private void* _eglDevice;
private void* _eglDisplay;
private void* _eglContext;
private void* _eglConfig;
private bool _es3;
private uint _swapInterval;
private readonly Dictionary<WindowId, WindowData> _windowData = new();
public override GLContextSpec[] SpecsToTry => Array.Empty<GLContextSpec>();
public override bool RequireWindowGL => false;
public override bool EarlyContextInit => true;
public override bool HasBrokenWindowSrgb => false;
public GLContextAngle(Clyde clyde) : base(clyde)
{
_sawmill = clyde._logManager.GetSawmill("clyde.ogl.angle");
}
public override GLContextSpec? SpecWithOpenGLVersion(RendererOpenGLVersion version)
{
// Do not initialize GL context on the window directly, we use ANGLE.
return null;
}
public override void UpdateVSync()
{
_swapInterval = (uint) (Clyde._vSync ? 1 : 0);
}
public override void WindowCreated(GLContextSpec? spec, WindowReg reg)
{
var data = new WindowData
{
Reg = reg
};
_windowData[reg.Id] = data;
var hWnd = (HWND) Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
// todo: exception management.
CreateSwapChain1(hWnd, data);
_factory->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER);
var rt = Clyde.RtToLoaded(reg.RenderTarget);
rt.FlipY = true;
if (reg.IsMainWindow)
{
UpdateVSync();
eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
}
}
private void DestroyBackbuffer(WindowData data)
{
if (data.EglBackbuffer != null)
{
if (data.Reg.IsMainWindow)
eglMakeCurrent(_eglDisplay, null, null, null);
eglDestroySurface(_eglDisplay, data.EglBackbuffer);
data.EglBackbuffer = null;
}
data.Backbuffer->Release();
data.Backbuffer = null;
}
private void SetupBackbuffer(WindowData data)
{
DebugTools.Assert(data.Backbuffer == null, "Backbuffer must have been released!");
DebugTools.Assert(data.EglBackbuffer == null, "EGL Backbuffer must have been released!");
fixed (ID3D11Texture2D** texPtr = &data.Backbuffer)
{
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, __uuidof<ID3D11Texture2D>(), (void**) texPtr));
}
var attributes = stackalloc int[]
{
EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
EGL_NONE
};
data.EglBackbuffer = eglCreatePbufferFromClientBuffer(
_eglDisplay,
EGL_D3D_TEXTURE_ANGLE,
data.Backbuffer,
_eglConfig,
attributes);
}
private void CreateSwapChain1(HWND hWnd, WindowData data)
{
var desc = new DXGI_SWAP_CHAIN_DESC
{
BufferDesc =
{
Width = (uint) data.Reg.FramebufferSize.X,
Height = (uint) data.Reg.FramebufferSize.Y,
Format = Clyde._hasGLSrgb ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT_R8G8B8A8_UNORM
},
SampleDesc =
{
Count = 1
},
OutputWindow = hWnd,
BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT,
BufferCount = 2,
SwapEffect = DXGI_SWAP_EFFECT_DISCARD,
Windowed = 1
};
fixed (IDXGISwapChain** swapPtr = &data.SwapChain)
{
ThrowIfFailed("CreateSwapChain", _factory->CreateSwapChain(
(IUnknown*) _device,
&desc,
swapPtr
));
}
SetupBackbuffer(data);
}
public override void WindowDestroyed(WindowReg reg)
{
var data = _windowData[reg.Id];
DestroyBackbuffer(data);
data.SwapChain->Release();
_windowData.Remove(reg.Id);
}
public bool TryInitialize()
{
try
{
TryInitializeCore();
}
catch (Exception e)
{
_sawmill.Error($"Failed to initialize custom ANGLE: {e}");
Shutdown();
return false;
}
return true;
}
public void EarlyInit()
{
// Early GL context init so that feature detection runs before window creation,
// and so that we can know _hasGLSrgb in window creation.
eglMakeCurrent(_eglDisplay, null, null, _eglContext);
Clyde.InitOpenGL();
Clyde._earlyGLInit = true;
}
private void TryInitializeCore()
{
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(null, EGL_EXTENSIONS));
_sawmill.Debug($"EGL client extensions: {extensions}!");
CreateD3D11Device();
CreateEglContext();
}
private void CreateEglContext()
{
_eglDevice = eglCreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, _device, null);
if (_eglDevice == (void*) EGL_NO_DEVICE_EXT)
throw new Exception("eglCreateDeviceANGLE failed.");
_eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, _eglDevice, null);
if (_eglDisplay == null)
throw new Exception("eglGetPlatformDisplayEXT failed.");
int major;
int minor;
if (eglInitialize(_eglDisplay, &major, &minor) == EGL_FALSE)
throw new Exception("eglInitialize failed.");
var vendor = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VENDOR));
var version = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VERSION));
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_EXTENSIONS));
_sawmill.Debug("EGL initialized!");
_sawmill.Debug($"EGL vendor: {vendor}!");
_sawmill.Debug($"EGL version: {version}!");
_sawmill.Debug($"EGL extensions: {extensions}!");
if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE)
throw new Exception("eglBindAPI failed.");
var attribs = stackalloc int[]
{
// EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
var numConfigs = 0;
if (eglChooseConfig(_eglDisplay, attribs, null, 0, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
var configs = stackalloc void*[numConfigs];
if (eglChooseConfig(_eglDisplay, attribs, configs, numConfigs, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
if (numConfigs == 0)
throw new Exception("No compatible EGL configurations returned!");
_sawmill.Debug($"{numConfigs} EGL configs possible!");
for (var i = 0; i < numConfigs; i++)
{
_sawmill.Debug(DumpEglConfig(_eglDisplay, configs[i]));
}
_eglConfig = configs[0];
int supportedRenderableTypes;
eglGetConfigAttrib(_eglDisplay, _eglConfig, EGL_RENDERABLE_TYPE, &supportedRenderableTypes);
_es3 = (supportedRenderableTypes & EGL_OPENGL_ES3_BIT) != 0;
var createAttribs = stackalloc int[]
{
EGL_CONTEXT_CLIENT_VERSION, _es3 ? 3 : 2,
EGL_NONE
};
_eglContext = eglCreateContext(_eglDisplay, _eglConfig, null, createAttribs);
if (_eglContext == (void*) EGL_NO_CONTEXT)
throw new Exception("eglCreateContext failed!");
_sawmill.Debug("EGL context created!");
Clyde._openGLVersion = _es3 ? RendererOpenGLVersion.GLES3 : RendererOpenGLVersion.GLES2;
}
private void CreateD3D11Device()
{
IDXGIDevice1* dxgiDevice = null;
try
{
fixed (IDXGIFactory1** ptr = &_factory)
{
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) ptr));
}
// Try to find the correct adapter if specified.
var adapterName = Clyde._cfg.GetCVar(CVars.DisplayAdapter);
if (adapterName != "")
{
_adapter = TryFindAdapterWithName(adapterName);
if (_adapter == null)
{
_sawmill.Warning($"Unable to find display adapter with requested name: {adapterName}");
}
_sawmill.Debug($"Found display adapter with name: {adapterName}");
}
#pragma warning disable CA1416
#pragma warning disable CS0162
IDXGIFactory6* factory6;
if (_adapter == null && _factory->QueryInterface(__uuidof<IDXGIFactory6>(), (void**) &factory6) == 0)
{
var gpuPref = (DXGI_GPU_PREFERENCE) Clyde._cfg.GetCVar(CVars.DisplayGpuPreference);
IDXGIAdapter1* adapter;
for (var adapterIndex = 0u;
factory6->EnumAdapterByGpuPreference(
adapterIndex,
gpuPref,
__uuidof<IDXGIAdapter1>(),
(void**)&adapter) != DXGI_ERROR_NOT_FOUND;
adapterIndex++)
{
/*
DXGI_ADAPTER_DESC1 aDesc;
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&aDesc));
var aDescName = new ReadOnlySpan<char>(aDesc.Description, 128);
Logger.DebugS("clyde.ogl.angle", aDescName.ToString());
adapter->Release();
*/
_adapter = adapter;
break;
}
factory6->Release();
}
#pragma warning restore CS0162
#pragma warning restore CA1416
Span<D3D_FEATURE_LEVEL> featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
// 11_0 can do GLES3
D3D_FEATURE_LEVEL_11_0,
// 9_3 can do GLES2
D3D_FEATURE_LEVEL_9_3,
// If we get a 9_1 FL we can't do D3D11 based ANGLE,
// but ANGLE can do it manually via the D3D9 renderer.
// In this case, abort custom swap chain and let ANGLE handle everything.
D3D_FEATURE_LEVEL_9_1
};
if (Clyde._cfg.GetCVar(CVars.DisplayAngleForceEs2))
{
featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
// Don't allow FL 11_0 so ANGLE is forced to init GLES2.
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_1
};
}
if (Clyde._cfg.GetCVar(CVars.DisplayAngleForce10_0))
{
featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
D3D_FEATURE_LEVEL_10_0,
};
}
fixed (ID3D11Device** device = &_device)
fixed (D3D_FEATURE_LEVEL* fl = &featureLevels[0])
{
ThrowIfFailed("D3D11CreateDevice", D3D11CreateDevice(
(IDXGIAdapter*) _adapter,
_adapter == null ? D3D_DRIVER_TYPE_HARDWARE : D3D_DRIVER_TYPE_UNKNOWN,
HMODULE.NULL,
0,
fl,
(uint) featureLevels.Length,
D3D11_SDK_VERSION,
device,
null,
null
));
}
// Get adapter from the device.
ThrowIfFailed("QueryInterface", _device->QueryInterface(__uuidof<IDXGIDevice1>(), (void**) &dxgiDevice));
fixed (IDXGIAdapter1** ptrAdapter = &_adapter)
{
ThrowIfFailed("GetParent", dxgiDevice->GetParent(__uuidof<IDXGIAdapter1>(), (void**) ptrAdapter));
}
_deviceFl = _device->GetFeatureLevel();
DXGI_ADAPTER_DESC1 desc;
ThrowIfFailed("GetDesc1", _adapter->GetDesc1(&desc));
var descName = ((ReadOnlySpan<char>)desc.Description).TrimEnd('\0');
_sawmill.Debug("Successfully created D3D11 device!");
_sawmill.Debug($"D3D11 Device Adapter: {descName.ToString()}");
_sawmill.Debug($"D3D11 Device FL: {_deviceFl}");
if (_deviceFl == D3D_FEATURE_LEVEL_9_1)
{
throw new Exception(
"D3D11 device has too low FL (need at least 9_3). Aborting custom swap chain!");
}
}
finally
{
if (dxgiDevice != null)
dxgiDevice->Release();
}
}
public override void Shutdown()
{
// Shut down ANGLE.
if (_eglDisplay != null)
eglTerminate(_eglDisplay);
if (_eglDevice != null)
eglReleaseDeviceANGLE(_eglDevice);
// Shut down D3D11/DXGI
if (_factory != null)
_factory->Release();
if (_adapter != null)
_adapter->Release();
if (_device != null)
_device->Release();
}
public override void SwapAllBuffers()
{
foreach (var data in _windowData.Values)
{
data.SwapChain->Present(_swapInterval, 0);
}
}
public override void WindowResized(WindowReg reg, Vector2i oldSize)
{
var data = _windowData[reg.Id];
DestroyBackbuffer(data);
ThrowIfFailed("ResizeBuffers", data.SwapChain->ResizeBuffers(
2,
(uint) reg.FramebufferSize.X, (uint) reg.FramebufferSize.Y,
Clyde._hasGLSrgb ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT_R8G8B8A8_UNORM,
0));
SetupBackbuffer(data);
if (reg.IsMainWindow)
eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
}
private IDXGIAdapter1* TryFindAdapterWithName(string name)
{
uint idx = 0;
while (true)
{
IDXGIAdapter1* adapter;
var hr = _factory->EnumAdapters1(idx++, &adapter);
if (hr == DXGI_ERROR_NOT_FOUND)
break;
ThrowIfFailed("EnumAdapters1", hr);
DXGI_ADAPTER_DESC1 desc;
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&desc));
var descName = ((ReadOnlySpan<char>)desc.Description);
if (descName.StartsWith(name))
return adapter;
adapter->Release();
}
return null;
}
public override void* GetProcAddress(string name)
{
Span<byte> buf = stackalloc byte[128];
var len = Encoding.UTF8.GetBytes(name, buf);
buf[len] = 0;
fixed (byte* ptr = &buf[0])
{
return eglGetProcAddress(ptr);
}
}
public override void BindWindowRenderTarget(WindowId rtWindowId)
{
var data = _windowData[rtWindowId];
var result = eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
if (result == EGL_FALSE)
throw new Exception("eglMakeCurrent failed.");
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
Clyde.CheckGlError();
}
private static void ThrowIfFailed(string methodName, HRESULT hr)
{
if (FAILED(hr))
{
Marshal.ThrowExceptionForHR(hr);
}
}
private static string DumpEglConfig(void* display, void* config)
{
var sb = new StringBuilder();
sb.Append($"cfg: {Get(EGL_CONFIG_ID):000} | ");
sb.AppendFormat(
"R/G/B/A/D/S: {0}/{1}/{2}/{3}/{4:00}/{5} | ",
Get(EGL_RED_SIZE), Get(EGL_GREEN_SIZE), Get(EGL_BLUE_SIZE), Get(EGL_ALPHA_SIZE),
Get(EGL_DEPTH_SIZE), Get(EGL_STENCIL_SIZE));
// COLOR_BUFFER_TYPE
sb.Append($"CBT: {Get(EGL_COLOR_BUFFER_TYPE)} | ");
sb.Append($"CC: {Get(EGL_CONFIG_CAVEAT)} | ");
sb.Append($"CONF: {Get(EGL_CONFORMANT)} | ");
sb.Append($"NAT: {Get(EGL_NATIVE_VISUAL_ID)} | ");
sb.Append($"SAMPLES: {Get(EGL_SAMPLES)} | ");
sb.Append($"SAMPLE_BUFFERS: {Get(EGL_SAMPLE_BUFFERS)} | ");
sb.Append($"ORIENTATION: {Get(EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE)} | ");
sb.Append($"RENDERABLE: {Get(EGL_RENDERABLE_TYPE)}");
return sb.ToString();
int Get(int attrib)
{
int value;
if (eglGetConfigAttrib(display, config, attrib, &value) == EGL_FALSE)
throw new Exception("eglGetConfigAttrib failed!");
return value;
}
}
private sealed class WindowData
{
public WindowReg Reg = default!;
public IDXGISwapChain* SwapChain;
public ID3D11Texture2D* Backbuffer;
public void* EglBackbuffer;
}
}
}
}

View File

@@ -1,118 +0,0 @@
using System;
using OpenToolkit;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Manages OpenGL contexts for the windowing system.
/// </summary>
private abstract class GLContextBase
{
protected readonly Clyde Clyde;
public IBindingsContext BindingsContext { get; }
public GLContextBase(Clyde clyde)
{
Clyde = clyde;
BindingsContext = new BindingsContextImpl(this);
}
public GLContextSpec? GetNewWindowSpec()
{
return SpecWithOpenGLVersion(Clyde._openGLVersion);
}
public virtual bool EarlyContextInit => false;
public abstract GLContextSpec? SpecWithOpenGLVersion(RendererOpenGLVersion version);
public abstract void UpdateVSync();
public abstract void WindowCreated(GLContextSpec? spec, WindowReg reg);
public abstract void WindowDestroyed(WindowReg reg);
public abstract void Shutdown();
public abstract GLContextSpec[] SpecsToTry { get; }
public abstract bool RequireWindowGL { get; }
public abstract bool HasBrokenWindowSrgb { get; }
protected static GLContextSpec GetVersionSpec(RendererOpenGLVersion version)
{
var spec = new GLContextSpec { OpenGLVersion = version };
switch (version)
{
case RendererOpenGLVersion.GL33:
spec.Major = 3;
spec.Minor = 3;
spec.Profile = GLContextProfile.Core;
spec.CreationApi = GLContextCreationApi.Native;
break;
case RendererOpenGLVersion.GL31:
spec.Major = 3;
spec.Minor = 1;
spec.Profile = GLContextProfile.Compatibility;
spec.CreationApi = GLContextCreationApi.Native;
break;
case RendererOpenGLVersion.GLES3:
spec.Major = 3;
spec.Minor = 0;
spec.Profile = GLContextProfile.Es;
// Initializing ES on Windows EGL so that we can use ANGLE.
spec.CreationApi = OperatingSystem.IsWindows()
? GLContextCreationApi.Egl
: GLContextCreationApi.Native;
break;
case RendererOpenGLVersion.GLES2:
spec.Major = 2;
spec.Minor = 0;
spec.Profile = GLContextProfile.Es;
// Initializing ES on Windows EGL so that we can use ANGLE.
spec.CreationApi = OperatingSystem.IsWindows()
? GLContextCreationApi.Egl
: GLContextCreationApi.Native;
break;
default:
throw new ArgumentOutOfRangeException();
}
return spec;
}
public abstract void SwapAllBuffers();
public abstract void WindowResized(WindowReg reg, Vector2i oldSize);
public abstract unsafe void* GetProcAddress(string name);
public abstract void BindWindowRenderTarget(WindowId rtWindowId);
public virtual void BeforeSharedWindowCreateUnbind()
{
}
private sealed class BindingsContextImpl : IBindingsContext
{
private readonly GLContextBase _context;
public BindingsContextImpl(GLContextBase context)
{
_context = context;
}
public unsafe IntPtr GetProcAddress(string procName)
{
return (nint)_context.GetProcAddress(procName);
}
}
}
}
}

View File

@@ -1,280 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using TerraFX.Interop.Windows;
using static Robust.Client.Graphics.Clyde.Egl;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Context manager that uses EGL directly so that we get better control over multi-window management.
/// </summary>
private sealed unsafe class GLContextEgl : GLContextBase
{
// TODO: Currently this class uses ANGLE and Windows-specific initialization code.
// It could be made more general purpose later if anybody ever gets adventurous with like, Wayland.
private readonly Dictionary<WindowId, WindowData> _windowData = new();
private readonly ISawmill _sawmill;
private void* _eglDisplay;
private void* _eglContext;
private void* _eglConfig;
public override bool HasBrokenWindowSrgb => Clyde._isGLES && OperatingSystem.IsWindows();
public GLContextEgl(Clyde clyde) : base(clyde)
{
_sawmill = clyde._logManager.GetSawmill("clyde.ogl.egl");
}
public override GLContextSpec? SpecWithOpenGLVersion(RendererOpenGLVersion version)
{
return null;
}
public override void UpdateVSync()
{
var interval = Clyde._vSync ? 1 : 0;
eglSwapInterval(_eglDisplay, interval);
}
public void InitializePublic()
{
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(null, EGL_EXTENSIONS));
_sawmill.Debug($"EGL client extensions: {extensions}!");
}
public override void WindowCreated(GLContextSpec? spec, WindowReg reg)
{
var data = new WindowData
{
Reg = reg
};
_windowData[reg.Id] = data;
if (reg.IsMainWindow)
Initialize(data);
var attribs = stackalloc int[]
{
EGL_GL_COLORSPACE, EGL_GL_COLORSPACE_SRGB,
EGL_NONE
};
if (OperatingSystem.IsWindows())
{
// Set up window surface.
var hWNd = Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
data.EglSurface = eglCreateWindowSurface(_eglDisplay, _eglConfig, (void*) hWNd, attribs);
if (data.EglSurface == (void*) EGL_NO_SURFACE)
throw new Exception("eglCreateWindowSurface failed.");
}
else if (OperatingSystem.IsLinux())
{
var window = Clyde._windowing!.WindowGetX11Id(reg)!.Value;
data.EglSurface = eglCreateWindowSurface(_eglDisplay, _eglConfig, (void*) window, attribs);
if (data.EglSurface == (void*) EGL_NO_SURFACE)
throw new Exception("eglCreateWindowSurface failed.");
}
else
{
throw new NotSupportedException("EGL is not currently supported outside Windows ANGLE or X11 Linux");
}
if (reg.IsMainWindow)
{
var result = eglMakeCurrent(_eglDisplay, data.EglSurface, data.EglSurface, _eglContext);
if (result == EGL_FALSE)
throw new Exception("eglMakeCurrent failed.");
}
}
public override void WindowDestroyed(WindowReg reg)
{
var data = _windowData[reg.Id];
eglDestroySurface(_eglDisplay, data.EglSurface);
}
private void Initialize(WindowData mainWindow)
{
if (OperatingSystem.IsWindows())
{
// Setting up ANGLE without manually selecting a D3D11 device requires a windows DC.
mainWindow.DC = Windows.GetDC((HWND)Clyde._windowing!.WindowGetWin32Window(mainWindow.Reg)!.Value);
_eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, (void*) mainWindow.DC, null);
if (_eglDisplay == null)
throw new Exception("eglGetPlatformDisplayEXT failed.");
}
else if (OperatingSystem.IsLinux())
{
var xDisplay = Clyde._windowing!.WindowGetX11Display(mainWindow.Reg)!.Value;
_eglDisplay = eglGetDisplay((void*) xDisplay);
if (mainWindow.EglSurface == (void*) EGL_NO_SURFACE)
throw new Exception("eglCreateWindowSurface failed.");
}
else
{
throw new NotSupportedException("EGL is not currently supported outside Windows ANGLE or X11 Linux");
}
int major;
int minor;
if (eglInitialize(_eglDisplay, &major, &minor) == EGL_FALSE)
throw new Exception("eglInitialize failed.");
var vendor = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VENDOR));
var version = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VERSION));
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_EXTENSIONS));
_sawmill.Debug("EGL initialized!");
_sawmill.Debug($"EGL vendor: {vendor}!");
_sawmill.Debug($"EGL version: {version}!");
_sawmill.Debug($"EGL extensions: {extensions}!");
if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE)
throw new Exception("eglBindAPI failed.");
var attribs = stackalloc int[]
{
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_NONE
};
var numConfigs = 0;
if (eglChooseConfig(_eglDisplay, attribs, null, 0, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
var configs = stackalloc void*[numConfigs];
if (eglChooseConfig(_eglDisplay, attribs, configs, numConfigs, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
if (numConfigs == 0)
throw new Exception("No compatible EGL configurations returned!");
_sawmill.Debug($"{numConfigs} EGL configs possible!");
for (var i = 0; i < numConfigs; i++)
{
_sawmill.Debug(DumpEglConfig(_eglDisplay, configs[i]));
}
_eglConfig = configs[0];
var createAttribs = stackalloc int[]
{
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
};
_eglContext = eglCreateContext(_eglDisplay, _eglConfig, null, createAttribs);
if (_eglContext == (void*) EGL_NO_CONTEXT)
throw new Exception("eglCreateContext failed!");
_sawmill.Debug("EGL context created!");
}
public override void Shutdown()
{
if (_eglDisplay != null)
{
eglMakeCurrent(_eglDisplay, null, null, null);
eglTerminate(_eglDisplay);
}
}
public override GLContextSpec[] SpecsToTry => Array.Empty<GLContextSpec>();
public override bool RequireWindowGL => false;
public override void SwapAllBuffers()
{
foreach (var data in _windowData.Values)
{
eglSwapBuffers(_eglDisplay, data.EglSurface);
}
}
public override void WindowResized(WindowReg reg, Vector2i oldSize)
{
// Nada..?
}
public override void* GetProcAddress(string name)
{
Span<byte> buf = stackalloc byte[128];
var len = Encoding.UTF8.GetBytes(name, buf);
buf[len] = 0;
fixed (byte* ptr = &buf[0])
{
return eglGetProcAddress(ptr);
}
}
public override void BindWindowRenderTarget(WindowId rtWindowId)
{
var data = _windowData[rtWindowId];
var result = eglMakeCurrent(_eglDisplay, data.EglSurface, data.EglSurface, _eglContext);
if (result == EGL_FALSE)
throw new Exception("eglMakeCurrent failed.");
}
private static string DumpEglConfig(void* display, void* config)
{
var sb = new StringBuilder();
sb.Append($"cfg: {Get(EGL_CONFIG_ID):00} | ");
sb.AppendFormat(
"R/G/B/A/D/S: {0}/{1}/{2}/{3}/{4:00}/{5} | ",
Get(EGL_RED_SIZE), Get(EGL_GREEN_SIZE), Get(EGL_BLUE_SIZE), Get(EGL_ALPHA_SIZE),
Get(EGL_DEPTH_SIZE), Get(EGL_STENCIL_SIZE));
// COLOR_BUFFER_TYPE
sb.Append($"CBT: {Get(EGL_COLOR_BUFFER_TYPE)} | ");
sb.Append($"CC: {Get(EGL_CONFIG_CAVEAT)} | ");
sb.Append($"CONF: {Get(EGL_CONFORMANT)} | ");
sb.Append($"NAT: {Get(EGL_NATIVE_VISUAL_ID)} | ");
sb.Append($"SAMPLES: {Get(EGL_SAMPLES)} | ");
sb.Append($"SAMPLE_BUFFERS: {Get(EGL_SAMPLE_BUFFERS)}");
return sb.ToString();
int Get(int attrib)
{
int value;
if (eglGetConfigAttrib(display, config, attrib, &value) == EGL_FALSE)
throw new Exception("eglGetConfigAttrib failed!");
return value;
}
}
private sealed class WindowData
{
public WindowReg Reg = default!;
// ReSharper disable once InconsistentNaming
// Windows DC for this window.
// Only used for main window.
public nint DC;
public void* EglSurface;
}
}
}
}

View File

@@ -1,356 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// GL Context(s) provided by the windowing system (GLFW, SDL2...)
/// </summary>
private sealed class GLContextWindow : GLContextBase
{
private readonly Dictionary<WindowId, WindowData> _windowData = new();
public override GLContextSpec[] SpecsToTry
{
get
{
// Compat mode: only GLES2.
if (Clyde._cfg.GetCVar(CVars.DisplayCompat))
{
return new[]
{
GetVersionSpec(RendererOpenGLVersion.GLES3),
GetVersionSpec(RendererOpenGLVersion.GLES2)
};
}
var requestedVersion = (RendererOpenGLVersion) Clyde._cfg.GetCVar(CVars.DisplayOpenGLVersion);
if (requestedVersion != RendererOpenGLVersion.Auto)
{
return new[]
{
GetVersionSpec(requestedVersion)
};
}
return new[]
{
GetVersionSpec(RendererOpenGLVersion.GL33),
GetVersionSpec(RendererOpenGLVersion.GL31),
GetVersionSpec(RendererOpenGLVersion.GLES3),
GetVersionSpec(RendererOpenGLVersion.GLES2),
};
}
}
public override bool RequireWindowGL => true;
// ANGLE does not support main window sRGB.
public override bool HasBrokenWindowSrgb => Clyde._isGLES && OperatingSystem.IsWindows();
public GLContextWindow(Clyde clyde) : base(clyde)
{
}
public override GLContextSpec? SpecWithOpenGLVersion(RendererOpenGLVersion version)
{
return GetVersionSpec(version);
}
public override void UpdateVSync()
{
if (Clyde._mainWindow == null)
return;
Clyde._windowing!.GLMakeContextCurrent(Clyde._mainWindow);
Clyde._windowing.GLSwapInterval(Clyde._mainWindow, Clyde._vSync ? 1 : 0);
}
public override void WindowCreated(GLContextSpec? spec, WindowReg reg)
{
reg.RenderTarget.MakeGLFence = true;
var data = new WindowData
{
Reg = reg
};
_windowData[reg.Id] = data;
if (reg.IsMainWindow)
{
Clyde._openGLVersion = spec!.Value.OpenGLVersion;
UpdateVSync();
}
else
{
Clyde._windowing!.GLMakeContextCurrent(Clyde._mainWindow);
CreateWindowRenderTexture(data);
InitWindowBlitThread(data);
}
}
public override void WindowDestroyed(WindowReg reg)
{
var data = _windowData[reg.Id];
data.BlitDoneEvent?.Set();
// Set events so blit thread properly wakes up and notices it needs to shut down.
data.BlitStartEvent?.Set();
_windowData.Remove(reg.Id);
}
public override void Shutdown()
{
// Nada, window system shutdown handles it.
}
public override void SwapAllBuffers()
{
BlitSecondaryWindows();
Clyde._windowing!.WindowSwapBuffers(Clyde._mainWindow!);
}
public override void WindowResized(WindowReg reg, Vector2i oldSize)
{
if (reg.IsMainWindow)
return;
// Recreate render texture for the window.
var data = _windowData[reg.Id];
data.RenderTexture!.Dispose();
CreateWindowRenderTexture(data);
}
public override unsafe void* GetProcAddress(string name)
{
return Clyde._windowing!.GLGetProcAddress(name);
}
public override void BindWindowRenderTarget(WindowId rtWindowId)
{
var data = _windowData[rtWindowId];
if (data.Reg.IsMainWindow)
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
Clyde.CheckGlError();
}
else
{
var loaded = Clyde.RtToLoaded(data.RenderTexture!);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, loaded.FramebufferHandle.Handle);
}
}
public override void BeforeSharedWindowCreateUnbind()
{
Clyde._windowing!.GLMakeContextCurrent(null);
}
private void BlitSecondaryWindows()
{
// Only got main window.
if (Clyde._windows.Count == 1)
return;
if (!Clyde._hasGLFenceSync && Clyde._cfg.GetCVar(CVars.DisplayForceSyncWindows))
{
GL.Finish();
}
if (Clyde.EffectiveThreadWindowBlit)
{
foreach (var window in _windowData.Values)
{
if (window.Reg.IsMainWindow)
continue;
window.BlitDoneEvent!.Reset();
window.BlitStartEvent!.Set();
window.BlitDoneEvent.Wait();
window.UnlockBeforeSwap = Clyde._cfg.GetCVar(CVars.DisplayThreadUnlockBeforeSwap);
}
}
else
{
foreach (var window in _windowData.Values)
{
if (window.Reg.IsMainWindow)
continue;
Clyde._windowing!.GLMakeContextCurrent(window.Reg);
BlitThreadDoSecondaryWindowBlit(window);
}
Clyde._windowing!.GLMakeContextCurrent(Clyde._mainWindow!);
}
}
private void BlitThreadDoSecondaryWindowBlit(WindowData window)
{
if (Clyde._hasGLFenceSync)
{
// 0xFFFFFFFFFFFFFFFFUL is GL_TIMEOUT_IGNORED
var rt = window.Reg.RenderTarget;
var sync = rt.LastGLSync;
GL.WaitSync(sync, WaitSyncFlags.None, unchecked((long) 0xFFFFFFFFFFFFFFFFUL));
Clyde.CheckGlError();
}
GL.Viewport(0, 0, window.Reg.FramebufferSize.X, window.Reg.FramebufferSize.Y);
Clyde.CheckGlError();
Clyde.SetTexture(TextureUnit.Texture0, window.RenderTexture!.Texture);
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
Clyde.CheckGlError();
if (window.UnlockBeforeSwap)
{
window.BlitDoneEvent?.Set();
}
Clyde._windowing!.WindowSwapBuffers(window.Reg);
if (!window.UnlockBeforeSwap)
{
window.BlitDoneEvent?.Set();
}
}
private unsafe void BlitThreadInit(WindowData reg)
{
Clyde._windowing!.GLMakeContextCurrent(reg.Reg);
Clyde._windowing.GLSwapInterval(reg.Reg, 0);
Clyde.SetupDebugCallback();
if (!Clyde._isGLES)
GL.Enable(EnableCap.FramebufferSrgb);
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, Clyde.WindowVBO.ObjectHandle);
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
var program = Clyde._compileProgram(
Clyde._winBlitShaderVert,
Clyde._winBlitShaderFrag,
new (string, uint)[]
{
("aPos", 0),
("tCoord", 1),
},
includeLib: false);
GL.UseProgram(program.Handle);
var loc = GL.GetUniformLocation(program.Handle, "tex");
Clyde.SetTexture(TextureUnit.Texture0, reg.RenderTexture!.Texture);
GL.Uniform1(loc, 0);
}
private void InitWindowBlitThread(WindowData reg)
{
if (Clyde.EffectiveThreadWindowBlit)
{
reg.BlitStartEvent = new ManualResetEventSlim();
reg.BlitDoneEvent = new ManualResetEventSlim();
reg.BlitThread = new Thread(() => BlitThread(reg))
{
Name = $"WinBlitThread ID:{reg.Reg.Id}",
IsBackground = true
};
// System.Console.WriteLine("A");
reg.BlitThread.Start();
// Wait for thread to finish init.
reg.BlitDoneEvent.Wait();
}
else
{
// Binds GL context.
BlitThreadInit(reg);
Clyde._windowing!.GLMakeContextCurrent(Clyde._mainWindow!);
}
}
private void BlitThread(WindowData reg)
{
BlitThreadInit(reg);
reg.BlitDoneEvent!.Set();
try
{
while (true)
{
reg.BlitStartEvent!.Wait();
if (reg.Reg.IsDisposed)
{
BlitThreadCleanup(reg);
return;
}
reg.BlitStartEvent!.Reset();
// Do channel blit.
BlitThreadDoSecondaryWindowBlit(reg);
}
}
catch (AggregateException e)
{
// ok channel closed, we exit.
e.Handle(ec => ec is ChannelClosedException);
}
}
private static void BlitThreadCleanup(WindowData reg)
{
reg.BlitDoneEvent!.Dispose();
reg.BlitStartEvent!.Dispose();
}
private void CreateWindowRenderTexture(WindowData reg)
{
reg.RenderTexture?.Dispose();
reg.RenderTexture = Clyde.CreateRenderTarget(
reg.Reg.FramebufferSize,
new RenderTargetFormatParameters
{
ColorFormat = RenderTargetColorFormat.Rgba8Srgb,
HasDepthStencil = true
},
name: $"{reg.Reg.Id}-RenderTexture");
// Necessary to correctly sync multi-context blitting.
reg.RenderTexture.MakeGLFence = true;
}
private sealed class WindowData
{
public WindowReg Reg = default!;
public RenderTexture? RenderTexture;
// Used EXCLUSIVELY to run the two rendering commands to blit to the window.
public Thread? BlitThread;
public ManualResetEventSlim? BlitStartEvent;
public ManualResetEventSlim? BlitDoneEvent;
public bool UnlockBeforeSwap;
}
}
}
}

View File

@@ -1,191 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Represents an OpenGL buffer object.
/// </summary>
[Virtual]
private class GLBuffer
{
private readonly Clyde _clyde;
public BufferTarget Type { get; }
public uint ObjectHandle { get; private set; }
public BufferUsageHint UsageHint { get; }
public string? Name { get; }
public GLBuffer(Clyde clyde, BufferTarget type, BufferUsageHint usage, string? name = null)
{
_clyde = clyde;
Type = type;
Name = name;
UsageHint = usage;
GL.GenBuffers(1, out uint handle);
clyde.CheckGlError();
ObjectHandle = handle;
GL.BindBuffer(type, handle);
clyde.CheckGlError();
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Buffer, ObjectHandle, name);
}
public GLBuffer(Clyde clyde, BufferTarget type, BufferUsageHint usage, int size, string? name = null)
: this(clyde, type, usage, name)
{
Reallocate(size);
}
public GLBuffer(Clyde clyde, BufferTarget type, BufferUsageHint usage, Span<byte> initialize,
string? name = null)
: this(clyde, type, usage, name)
{
Reallocate(initialize);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Use()
{
DebugTools.Assert(ObjectHandle != 0);
GL.BindBuffer(Type, ObjectHandle);
_clyde.CheckGlError();
}
public void Delete()
{
GL.DeleteBuffer(ObjectHandle);
_clyde.CheckGlError();
ObjectHandle = 0;
}
/// <summary>
/// <c>glBufferSubData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSubData<T>(int start, Span<T> data) where T : unmanaged
{
Use();
var byteSpan = MemoryMarshal.AsBytes(data);
unsafe
{
fixed (byte* ptr = byteSpan)
{
GL.BufferSubData(Type, (IntPtr) start, byteSpan.Length, (IntPtr) ptr);
}
}
_clyde.CheckGlError();
}
/// <summary>
/// <c>glBufferSubData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSubData<T>(Span<T> data) where T : unmanaged
{
Use();
var byteSpan = MemoryMarshal.AsBytes(data);
unsafe
{
fixed (byte* ptr = byteSpan)
{
GL.BufferSubData(Type, IntPtr.Zero, byteSpan.Length, (IntPtr) ptr);
}
}
_clyde.CheckGlError();
}
/// <summary>
/// <c>glBufferSubData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSubData<T>(in T data) where T : unmanaged
{
Use();
unsafe
{
fixed (T* ptr = &data)
{
GL.BufferSubData(Type, IntPtr.Zero, sizeof(T), (IntPtr) ptr);
}
}
_clyde.CheckGlError();
}
/// <summary>
/// <c>glBufferData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reallocate<T>(Span<T> data) where T : unmanaged
{
Use();
var byteSpan = MemoryMarshal.AsBytes(data);
unsafe
{
fixed (byte* ptr = byteSpan)
{
GL.BufferData(Type, byteSpan.Length, (IntPtr) ptr, UsageHint);
}
}
_clyde.CheckGlError();
}
/// <summary>
/// <c>glBufferData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reallocate<T>(in T data) where T : unmanaged
{
Use();
unsafe
{
fixed (T* ptr = &data)
{
GL.BufferData(Type, sizeof(T), (IntPtr) ptr, UsageHint);
}
}
_clyde.CheckGlError();
}
/// <summary>
/// <c>glBufferData</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reallocate(int size)
{
Use();
GL.BufferData(Type, size, IntPtr.Zero, UsageHint);
_clyde.CheckGlError();
}
}
/// <inheritdoc />
/// <summary>
/// Subtype of buffers so that we can have a generic constructor.
/// Functionally equivalent to <see cref="GLBuffer"/> otherwise.
/// </summary>
private sealed class GLBuffer<T> : GLBuffer where T : unmanaged
{
public GLBuffer(Clyde clyde, BufferTarget type, BufferUsageHint usage, Span<T> initialize,
string? name = null)
: base(clyde, type, usage, MemoryMarshal.AsBytes(initialize), name)
{
}
}
}
}

View File

@@ -1,60 +0,0 @@
using System;
using JetBrains.Annotations;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
// Go through the commit log if you wanna find why this struct exists.
// And why there's no implicit operator.
/// <summary>
/// Basically just a handle around the integer object handles returned by OpenGL.
/// </summary>
[PublicAPI]
internal struct GLHandle : IEquatable<GLHandle>
{
public readonly uint Handle;
public GLHandle(int handle) : this((uint) handle)
{
}
public GLHandle(uint handle)
{
Handle = handle;
}
public bool Equals(GLHandle other)
{
return Handle == other.Handle;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is GLHandle other && Equals(other);
}
public override int GetHashCode()
{
return Handle.GetHashCode();
}
public override string ToString()
{
return $"{nameof(GLHandle)}: {Handle}";
}
public static bool operator ==(GLHandle a, GLHandle b)
{
return a.Handle == b.Handle;
}
public static bool operator !=(GLHandle a, GLHandle b)
{
return a.Handle != b.Handle;
}
}
}
}

View File

@@ -1,56 +0,0 @@
using OpenToolkit.Graphics.OpenGL4;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed class GLShader
{
private readonly Clyde _clyde;
public GLShader(Clyde clyde, ShaderType type, string shaderSource, string? name=null)
{
_clyde = clyde;
Compile(type, shaderSource);
if (name != null)
{
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Shader, ObjectHandle, name);
}
}
public uint ObjectHandle { get; private set; } = 0;
public ShaderType Type { get; private set; }
private void Compile(ShaderType type, string shaderSource)
{
ObjectHandle = (uint)GL.CreateShader(type);
Type = type;
GL.ShaderSource((int) ObjectHandle, shaderSource);
_clyde.CheckGlError();
GL.CompileShader(ObjectHandle);
_clyde.CheckGlError();
GL.GetShader(ObjectHandle, ShaderParameter.CompileStatus, out var compiled);
_clyde.CheckGlError();
if (compiled != 1)
{
var message = GL.GetShaderInfoLog((int) ObjectHandle);
_clyde.CheckGlError();
Delete();
throw new ShaderCompilationException(message);
}
}
public void Delete()
{
if (ObjectHandle == 0)
{
return;
}
GL.DeleteShader(ObjectHandle);
_clyde.CheckGlError();
ObjectHandle = 0;
}
}
}
}

View File

@@ -1,625 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// This is an utility class. It does not check whether the OpenGL state machine is set up correctly.
/// You've been warned:
/// using things like <see cref="SetUniformTexture" /> if this buffer isn't bound WILL mess things up!
/// </summary>
private sealed class GLShaderProgram
{
private readonly sbyte?[] _uniformIntCache = new sbyte?[UniCount];
private readonly Dictionary<string, int> _uniformCache = new();
public uint Handle = 0;
private GLShader? _fragmentShader;
private GLShader? _vertexShader;
public string? Name { get; }
private readonly Clyde _clyde;
public GLShaderProgram(Clyde clyde, string? name = null)
{
_clyde = clyde;
Name = name;
}
public void Add(GLShader shader)
{
ClearCaches();
switch (shader.Type)
{
case ShaderType.VertexShader:
_vertexShader = shader;
break;
case ShaderType.FragmentShader:
_fragmentShader = shader;
break;
default:
throw new NotImplementedException("Tried to add unsupported shader type!");
}
}
public void Link((string, uint)[] attribLocations)
{
ClearCaches();
Handle = (uint) GL.CreateProgram();
_clyde.CheckGlError();
if (Name != null)
{
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Program, Handle, Name);
}
if (_vertexShader != null)
{
GL.AttachShader(Handle, _vertexShader.ObjectHandle);
_clyde.CheckGlError();
}
if (_fragmentShader != null)
{
GL.AttachShader(Handle, _fragmentShader.ObjectHandle);
_clyde.CheckGlError();
}
foreach (var (varName, loc) in attribLocations)
{
// OpenGL 3.1 is ass and doesn't allow you to specify layout(location = X) in shaders.
// So we have to manually do it here.
// Ugh.
GL.BindAttribLocation(Handle, loc, varName);
_clyde.CheckGlError();
}
GL.LinkProgram(Handle);
_clyde.CheckGlError();
GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out var compiled);
_clyde.CheckGlError();
if (compiled != 1)
{
throw new ShaderCompilationException(GL.GetProgramInfoLog((int) Handle));
}
}
public void Use()
{
if (_clyde._currentProgram == this)
{
return;
}
ForceUse();
}
public void ForceUse()
{
DebugTools.Assert(Handle != 0);
_clyde._currentProgram = this;
GL.UseProgram(Handle);
_clyde.CheckGlError();
}
public void Delete()
{
if (Handle == 0)
{
return;
}
GL.DeleteProgram(Handle);
_clyde.CheckGlError();
Handle = 0;
}
public int GetUniform(string name)
{
if (!TryGetUniform(name, out var result))
{
ThrowCouldNotGetUniform(name);
}
return result;
}
public int GetUniform(int id)
{
if (!TryGetUniform(id, out var result))
{
ThrowCouldNotGetUniform($"[id {id}]");
}
return result;
}
public bool TryGetUniform(string name, out int index)
{
DebugTools.Assert(Handle != 0);
if (_uniformCache.TryGetValue(name, out index))
{
return index != -1;
}
index = GL.GetUniformLocation(Handle, name);
_clyde.CheckGlError();
_uniformCache.Add(name, index);
return index != -1;
}
public bool TryGetUniform(int id, out int index)
{
DebugTools.Assert(Handle != 0);
DebugTools.Assert(id < UniCount);
var value = _uniformIntCache[id];
if (value.HasValue)
{
index = value.Value;
return index != -1;
}
return InitIntUniform(id, out index);
}
private bool InitIntUniform(int id, out int index)
{
string name;
switch (id)
{
case UniIModUV:
name = UniModUV;
break;
case UniILightTexture:
name = UniLightTexture;
break;
case UniIMainTexture:
name = UniMainTexture;
break;
case UniIModelMatrix:
name = UniModelMatrix;
break;
case UniITexturePixelSize:
name = UniTexturePixelSize;
break;
default:
throw new ArgumentOutOfRangeException();
}
index = GL.GetUniformLocation(Handle, name);
_clyde.CheckGlError();
_uniformIntCache[id] = (sbyte)index;
return index != -1;
}
public bool HasUniform(string name) => TryGetUniform(name, out _);
public bool HasUniform(int id) => TryGetUniform(id, out _);
public void BindBlock(string blockName, uint blockBinding)
{
var index = (uint) GL.GetUniformBlockIndex(Handle, blockName);
_clyde.CheckGlError();
GL.UniformBlockBinding(Handle, index, blockBinding);
_clyde.CheckGlError();
}
public void SetUniform(string uniformName, int integer)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, integer);
_clyde.CheckGlError();
}
public void SetUniform(string uniformName, float single)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, single);
_clyde.CheckGlError();
}
public void SetUniform(int uniformName, float single)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, single);
}
public void SetUniform(string uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
_clyde.CheckGlError();
}
public void SetUniform(int uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
}
public void SetUniform(string uniformName, in Matrix3x2 matrix)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix);
}
public void SetUniform(int uniformName, in Matrix3x2 matrix)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void SetUniformDirect(int slot, in Matrix3x2 value)
{
// We put the rows of the input matrix into the columns of our GPU matrices
// this transpose is required, as in C#, we premultiply vectors with matrices
// (vM) while GL postmultiplies vectors with matrices (Mv); however, since
// the Matrix3x2 data is stored row-major, and GL uses column-major, the
// memory layout is the same (or would be, if Matrix3x2 didn't have an
// implicit column)
var buf = stackalloc float[9]{
value.M11, value.M12, 0,
value.M21, value.M22, 0,
value.M31, value.M32, 1
};
GL.UniformMatrix3(slot, 1, false, (float*)buf);
_clyde.CheckGlError();
}
public void SetUniform(string uniformName, in Matrix4x4 matrix, bool transpose=true)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix, transpose);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void SetUniformDirect(int uniformId, in Matrix4x4 value, bool transpose=true)
{
Matrix4x4 tmpTranspose = value;
if (transpose)
{
// transposition not supported on GLES2, & no access to _hasGLES
tmpTranspose = Matrix4x4.Transpose(value);
}
GL.UniformMatrix4(uniformId, 1, false, (float*) &tmpTranspose);
_clyde.CheckGlError();
}
public void SetUniform(string uniformName, in Vector4 vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
public void SetUniform(int uniformName, in Vector4 vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Vector4 vector)
{
unsafe
{
fixed (Vector4* ptr = &vector)
{
GL.Uniform4(slot, 1, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Color color, bool convertToLinear = true)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, color, convertToLinear);
}
public void SetUniform(int uniformName, in Color color, bool convertToLinear = true)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, color, convertToLinear);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Color color, bool convertToLinear = true)
{
var converted = color;
if (convertToLinear)
{
converted = Color.FromSrgb(color);
}
unsafe
{
GL.Uniform4(slot, 1, (float*) &converted);
_clyde.CheckGlError();
}
}
public void SetUniform(string uniformName, Color[] colors)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, colors);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Color[] colors, bool convertToLinear = true)
{
scoped Span<Color> colorsToPass;
if (convertToLinear)
{
colorsToPass = stackalloc Color[colors.Length];
for (int i = 0; i < colors.Length; i++)
{
colorsToPass[i] = Color.FromSrgb(colors[i]);
}
}
else
{
colorsToPass = colors;
}
unsafe
{
fixed (Color* ptr = &colorsToPass[0])
{
GL.Uniform4(slot, colorsToPass.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Vector3 vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Vector3 vector)
{
unsafe
{
fixed (Vector3* ptr = &vector)
{
GL.Uniform3(slot, 1, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Vector2 vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
public void SetUniform(int uniformName, in Vector2 vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Vector2 vector)
{
unsafe
{
fixed (Vector2* ptr = &vector)
{
GL.Uniform2(slot, 1, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
public void SetUniform(int uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Vector2[] vectors)
{
unsafe
{
fixed (Vector2* ptr = &vectors[0])
{
GL.Uniform2(slot, vectors.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, bool[] bools)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, bools);
}
public void SetUniform(int uniformName, bool[] bools)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, bools);
}
private void SetUniformDirect(int slot, bool[] bools)
{
Span<int> intBools = stackalloc int[bools.Length];
for (var i = 0; i < bools.Length; i++)
{
intBools[i] = bools[i] ? 1 : 0;
}
unsafe
{
fixed (int* intBoolsPtr = intBools)
{
GL.Uniform1(slot, bools.Length, intBoolsPtr);
_clyde.CheckGlError();
}
}
}
public void SetUniformTexture(string uniformName, TextureUnit textureUnit)
{
var uniformId = GetUniform(uniformName);
SetUniformTextureDirect(uniformId, textureUnit);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformTextureDirect(int slot, TextureUnit value)
{
GL.Uniform1(slot, value - TextureUnit.Texture0);
_clyde.CheckGlError();
}
public void SetUniformTextureMaybe(string uniformName, TextureUnit value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformTextureDirect(slot, value);
}
}
public void SetUniformTextureMaybe(int uniformName, TextureUnit value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformTextureDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Vector4 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(int uniformName, in Vector4 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Color value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(int uniformName, in Color value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Matrix3x2 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Matrix4x4 value, bool transpose=true)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value, transpose);
}
}
public void SetUniformMaybe(int uniformName, in Matrix3x2 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Vector2 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, in Vector2i value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(int uniformName, in Vector2 value)
{
if (TryGetUniform(uniformName, out var slot))
{
SetUniformDirect(slot, value);
}
}
public void SetUniformMaybe(string uniformName, int value)
{
if (TryGetUniform(uniformName, out var slot))
{
GL.Uniform1(slot, value);
_clyde.CheckGlError();
}
}
public void SetUniformMaybe(string uniformName, float value)
{
if (TryGetUniform(uniformName, out var slot))
{
GL.Uniform1(slot, value);
_clyde.CheckGlError();
}
}
private void ClearCaches()
{
_uniformCache.Clear();
for (var i = 0; i < UniCount; i++)
{
_uniformIntCache[i] = null;
}
}
private static void ThrowCouldNotGetUniform(string name)
{
throw new ArgumentException($"Could not get uniform \"{name}\"!");
}
}
}
}

View File

@@ -1,90 +0,0 @@
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Represents some set of uniforms that can be backed by a uniform buffer or by regular uniforms.
/// Implies you're using this on a properly setup struct.
/// </summary>
private interface IAppliableUniformSet
{
/// <summary>
/// Applies the uniform set directly to a program.
/// </summary>
void Apply(Clyde clyde, GLShaderProgram program);
}
/// <summary>
/// Represents some set of uniforms that can be backed by a uniform buffer or by regular uniforms.
/// </summary>
private sealed class GLUniformBuffer<T> where T : unmanaged, IAppliableUniformSet
{
private readonly Clyde _clyde;
private readonly int _index;
/// <summary>
/// GPU Buffer (only used when uniform buffers are available)
/// </summary>
private GLBuffer? _implUBO = null;
/// <summary>
/// Mirror (only used when uniform buffers are unavailable)
/// </summary>
private T _implMirror;
public GLUniformBuffer(Clyde clyde, int index, string? name = null)
{
_clyde = clyde;
_index = index;
if (_clyde._hasGLUniformBuffers)
{
_implUBO = new GLBuffer(_clyde, BufferTarget.UniformBuffer, BufferUsageHint.StreamDraw, name);
unsafe {
_implUBO.Reallocate(sizeof(T));
}
Rebind();
}
}
public void Rebind()
{
if (_implUBO != null)
{
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, _index, (int) _implUBO!.ObjectHandle);
}
}
/// <summary>
/// Updates the buffer contents.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reallocate(in T data)
{
if (_implUBO != null)
{
_implUBO.Reallocate(data);
}
else
{
_implMirror = data;
}
}
/// <summary>
/// This is important for use on GLES2 - it ensures the uniforms in the specific program are up-to-date.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Apply(GLShaderProgram program)
{
if (_implUBO == null)
{
_implMirror.Apply(_clyde, program);
}
}
}
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared.Collections;
using Robust.Shared.Exceptions;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde;
internal sealed class GpuExpansionBuffer : IDisposable
{
private readonly RhiBase _rhi;
private readonly RhiBufferUsageFlags _usage;
private readonly string? _label;
private readonly byte[] _tempBuffer;
private int IndividualBufferSize => _tempBuffer.Length;
private ValueList<RhiBuffer> _gpuBuffers;
private int _curBufferPos;
private int _curBufferIdx;
public GpuExpansionBuffer(
RhiBase rhi,
int individualBufferSize,
RhiBufferUsageFlags usage,
string? label = null)
{
if ((usage & RhiBufferUsageFlags.CopyDst) == 0)
throw new ArgumentException($"Buffer usages must include {nameof(RhiBufferUsageFlags.CopyDst)}");
_rhi = rhi;
_usage = usage;
_label = label;
_tempBuffer = new byte[individualBufferSize];
// Make sure we have at least one buffer to start with.
AllocateBuffer();
DebugTools.Assert(_curBufferIdx < _gpuBuffers.Count, "Must have one buffer after creation");
}
public void Flush()
{
if (_curBufferPos == 0)
return;
FlushCurrentBuffer();
_curBufferIdx = 0;
// TODO: automatically cull GPU buffers if they remain unused for many frames.
}
// TODO: Verify this is safe to expose to content.
public Span<T> Allocate<T>(int count, out Position position)
where T : unmanaged
{
return AllocateAligned<T>(count, 1, out position);
}
// TODO: Verify this is safe to expose to content.
public unsafe Span<T> AllocateAligned<T>(int count, int alignment, out Position position)
where T : unmanaged
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
throw new TypeArgumentException();
var length = checked(count * sizeof(T));
if (length > IndividualBufferSize)
throw new ArgumentException("Requested allocation is larger than what could fit inside a single buffer.");
var posRounded = MathHelper.CeilingPowerOfTwo(_curBufferPos, alignment);
if (posRounded + length > IndividualBufferSize)
{
FlushCurrentBuffer();
DebugTools.Assert(_curBufferPos == 0, "We must have a fresh buffer now");
posRounded = 0;
}
position = new Position(_gpuBuffers[_curBufferIdx], posRounded);
var byteSpan = _tempBuffer.AsSpan(posRounded, length);
_curBufferPos = posRounded + length;
var itemSpan = MemoryMarshal.Cast<byte, T>(byteSpan);
DebugTools.Assert(itemSpan.Length == count);
DebugTools.Assert(_curBufferPos <= _tempBuffer.Length);
return itemSpan;
}
private void FlushCurrentBuffer()
{
DebugTools.Assert(_curBufferPos != 0, "We have nothing written, flushing the buffer makes no sense!");
// Temp buffer full. Upload buffer to GPU
var curBuffer = _gpuBuffers[_curBufferIdx];
_rhi.Queue.WriteBuffer(curBuffer, 0, _tempBuffer.AsSpan(0, _curBufferPos));
_curBufferIdx += 1;
if (_curBufferIdx == _gpuBuffers.Count)
{
// Out of spare buffers, make a new one.
AllocateBuffer();
DebugTools.Assert(_curBufferIdx < _gpuBuffers.Count, "Must have one buffer after creation");
}
_curBufferPos = 0;
}
private RhiBuffer AllocateBuffer()
{
var idx = _gpuBuffers.Count;
var buffer = _rhi.CreateBuffer(new RhiBufferDescriptor(
(ulong) IndividualBufferSize,
_usage,
false,
_label == null ? null : $"{_label}-{idx}")
);
_gpuBuffers.Add(buffer);
return buffer;
}
public void Dispose()
{
foreach (var gpuBuffer in _gpuBuffers)
{
gpuBuffer.Dispose();
}
_gpuBuffers.Clear();
}
public record struct Position(RhiBuffer Buffer, int ByteOffset);
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
namespace Robust.Client.Graphics.Clyde.Rhi;
public abstract partial class RhiBase
{
//
// Clyde <-> RHI API.
//
internal abstract void Init();
internal abstract void Shutdown();
/// <summary>
/// A window was created by Clyde. It should be initialized by the RHI to make it ready for rendering.
/// </summary>
/// <remarks>
/// Does not get called for the main window.
/// </remarks>
internal abstract void WindowCreated(Clyde.WindowReg reg);
/// <summary>
/// A window is about to be destroyed by Clyde. Clean up resources for it.
/// </summary>
internal abstract void WindowDestroy(Clyde.WindowReg reg);
/// <summary>
/// Recreate the native swap chain, in case it has become suboptimal (e.g. due to window resizing).
/// </summary>
internal abstract void WindowRecreateSwapchain(Clyde.WindowReg reg);
internal abstract RhiTextureView CreateTextureViewForWindow(Clyde.WindowReg reg);
internal abstract void WindowPresent(Clyde.WindowReg reg);
//
// RHI-internal API to de-OOP the public RHI API.
//
internal abstract RhiRenderPassEncoder CommandEncoderBeginRenderPass(
RhiCommandEncoder encoder,
in RhiRenderPassDescriptor descriptor
);
internal abstract RhiCommandBuffer CommandEncoderFinish(in RhiCommandEncoder encoder,
in RhiCommandBufferDescriptor descriptor);
internal abstract void RenderPassEncoderSetPipeline(
in RhiRenderPassEncoder encoder,
RhiRenderPipeline pipeline
);
internal abstract void RenderPassEncoderDraw(
in RhiRenderPassEncoder encoder,
uint vertexCount,
uint instanceCount,
uint firstVertex,
uint firstInstance
);
internal abstract void RenderPassEncoderEnd(RhiRenderPassEncoder encoder);
internal abstract void QueueSubmit(RhiQueue queue, RhiCommandBuffer[] commandBuffers);
internal abstract void QueueWriteTexture(
RhiQueue queue,
in RhiImageCopyTexture destination,
ReadOnlySpan<byte> data,
in RhiImageDataLayout dataLayout,
RhiExtent3D size
);
public abstract void QueueWriteBuffer(RhiBuffer buffer, ulong bufferOffset, ReadOnlySpan<byte> data);
internal abstract RhiTextureView TextureCreateView(RhiTexture texture, in RhiTextureViewDescriptor descriptor);
internal abstract void TextureViewDrop(RhiTextureView textureView);
internal abstract void BindGroupDrop(RhiBindGroup rhiBindGroup);
internal abstract void RenderPassEncoderSetBindGroup(
RhiRenderPassEncoder encoder,
uint index,
RhiBindGroup? bindGroup
);
internal abstract void RenderPassEncoderSetVertexBuffer(RhiRenderPassEncoder encoder,
uint slot,
RhiBuffer? buffer,
ulong offset,
ulong? size);
internal abstract void RenderPassEncoderSetScissorRect(RhiRenderPassEncoder encoder,
uint x,
uint y,
uint w,
uint h);
internal abstract void CommandBufferDrop(RhiCommandBuffer commandBuffer);
internal abstract RhiBufferMapState BufferGetMapState(RhiBuffer buffer);
internal abstract ValueTask BufferMapAsync(RhiBuffer buffer, RhiMapModeFlags mode, nuint offset, nuint size);
internal abstract RhiMappedBufferRange BufferGetMappedRange(RhiBuffer buffer, nuint offset, nuint size);
internal abstract void BufferUnmap(RhiBuffer buffer);
internal abstract void BufferDrop(RhiBuffer buffer);
}
internal record struct RhiHandle(long Value);

View File

@@ -0,0 +1,31 @@
using System.Numerics;
namespace Robust.Client.Graphics.Clyde.Rhi;
/// <summary>
/// Equivalent to a WGSL <c>mat3x2f</c>.
/// </summary>
/// <remarks>
/// This matrix is columnar and 3 columns, 2 rows. This is different from the rest of the engine and .NET.
/// </remarks>
public struct ShaderMat3x2F
{
public float M11;
public float M12;
public float M21;
public float M22;
public float M31;
public float M32;
public static ShaderMat3x2F Transpose(in Matrix3x2 matrix)
{
var ret = default(ShaderMat3x2F);
ret.M11 = matrix.M11;
ret.M12 = matrix.M12;
ret.M21 = matrix.M21;
ret.M22 = matrix.M22;
ret.M31 = matrix.M31;
ret.M32 = matrix.M32;
return ret;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, BindGroupLayoutReg> _bindGroupLayoutRegistry = new();
private readonly Dictionary<RhiHandle, BindGroupReg> _bindGroupRegistry = new();
internal override void BindGroupDrop(RhiBindGroup rhiBindGroup)
{
_wgpu.BindGroupDrop(_bindGroupRegistry[rhiBindGroup.Handle].Native);
_bindGroupRegistry.Remove(rhiBindGroup.Handle);
}
public override RhiBindGroupLayout CreateBindGroupLayout(in RhiBindGroupLayoutDescriptor descriptor)
{
Span<byte> buffer = stackalloc byte[512];
var pDescriptor = BumpAllocate<BindGroupLayoutDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
var entries = descriptor.Entries;
pDescriptor->EntryCount = (uint)entries.Length;
pDescriptor->Entries = BumpAllocate<BindGroupLayoutEntry>(ref buffer, entries.Length);
for (var i = 0; i < entries.Length; i++)
{
ref var entry = ref entries[i];
var pEntry = &pDescriptor->Entries[i];
pEntry->Binding = entry.Binding;
pEntry->Visibility = (ShaderStage)entry.Visibility;
switch (entry.Layout)
{
case RhiSamplerBindingLayout sampler:
pEntry->Sampler.Type = (SamplerBindingType)sampler.Type;
break;
case RhiTextureBindingLayout texture:
pEntry->Texture.Multisampled = texture.Multisampled;
pEntry->Texture.SampleType = (TextureSampleType)texture.SampleType;
pEntry->Texture.ViewDimension =
(TextureViewDimension)ValidateTextureViewDimension(texture.ViewDimension);
break;
case RhiBufferBindingLayout layoutBuffer:
pEntry->Buffer.Type = (BufferBindingType) layoutBuffer.Type;
pEntry->Buffer.HasDynamicOffset = layoutBuffer.HasDynamicOffset;
pEntry->Buffer.MinBindingSize = layoutBuffer.MinBindingSize;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var native = _webGpu.DeviceCreateBindGroupLayout(_wgpuDevice, pDescriptor);
// TODO: Thread safety
var handle = AllocRhiHandle();
_bindGroupLayoutRegistry.Add(handle, new BindGroupLayoutReg { Native = native });
return new RhiBindGroupLayout(this, handle);
}
public override RhiBindGroup CreateBindGroup(in RhiBindGroupDescriptor descriptor)
{
// TODO: SAFETY
Span<byte> buffer = stackalloc byte[1024];
var pDescriptor = BumpAllocate<BindGroupDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
pDescriptor->Layout = _bindGroupLayoutRegistry[descriptor.Layout.Handle].Native;
var entries = descriptor.Entries;
pDescriptor->EntryCount = (uint) entries.Length;
pDescriptor->Entries = BumpAllocate<BindGroupEntry>(ref buffer, entries.Length);
for (var i = 0; i < entries.Length; i++)
{
ref var entry = ref descriptor.Entries[i];
var pEntry = &pDescriptor->Entries[i];
pEntry->Binding = entry.Binding;
switch (entry.Resource)
{
case RhiSampler rhiSampler:
pEntry->Sampler = _samplerRegistry[rhiSampler.Handle].Native;
break;
case RhiTextureView rhiTextureView:
pEntry->TextureView = _textureViewRegistry[rhiTextureView.Handle].Native;
break;
case RhiBufferBinding bufferBinding:
pEntry->Buffer = _bufferRegistry[bufferBinding.Buffer.Handle].Native;
pEntry->Offset = bufferBinding.Offset;
pEntry->Size = bufferBinding.Size ?? WebGPU.WholeSize;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var bindGroup = _webGpu.DeviceCreateBindGroup(_wgpuDevice, pDescriptor);
var handle = AllocRhiHandle();
_bindGroupRegistry.Add(handle, new BindGroupReg { Native = bindGroup });
return new RhiBindGroup(this, handle);
}
private sealed class BindGroupLayoutReg
{
public BindGroupLayout* Native;
}
private sealed class BindGroupReg
{
public BindGroup* Native;
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Internal.TypeSystem;
using Silk.NET.WebGPU;
using Buffer = Silk.NET.WebGPU.Buffer;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, BufferReg> _bufferRegistry = new();
public override unsafe RhiBuffer CreateBuffer(in RhiBufferDescriptor descriptor)
{
Span<byte> buffer = stackalloc byte[512];
var pDescriptor = BumpAllocate<BufferDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
pDescriptor->MappedAtCreation = descriptor.MappedAtCreation;
pDescriptor->Size = descriptor.Size;
pDescriptor->Usage = (BufferUsage) descriptor.Usage;
var native = _webGpu.DeviceCreateBuffer(_wgpuDevice, pDescriptor);
var handle = AllocRhiHandle();
_bufferRegistry.Add(handle, new BufferReg { Native = native });
var rhiBuffer= new RhiBuffer(this, handle);
if (pDescriptor->MappedAtCreation)
{
rhiBuffer.Mapping = new RhiBuffer.ActiveMapping(rhiBuffer) { Valid = true };
}
return rhiBuffer;
}
internal override unsafe RhiBufferMapState BufferGetMapState(RhiBuffer buffer)
{
var nativeBuffer = _bufferRegistry[buffer.Handle].Native;
return (RhiBufferMapState) _webGpu.BufferGetMapState(nativeBuffer);
}
internal override async ValueTask BufferMapAsync(RhiBuffer buffer, RhiMapModeFlags mode, nuint offset, nuint size)
{
// TODO: Probably need some more locks here idk.
// So people can't map the buffer at the same time as or something.
buffer.MapState = RhiBufferMapState.Pending;
WgpuMapBufferAsyncResult result;
using (var promise = new WgpuPromise<WgpuMapBufferAsyncResult>())
{
unsafe
{
var nativeBuffer = _bufferRegistry[buffer.Handle].Native;
_webGpu.BufferMapAsync(
nativeBuffer,
(MapMode) mode,
offset,
size,
new PfnBufferMapCallback(&WgpuMapBufferAsyncCallback),
promise.UserData
);
}
// TODO: are we handling the error correctly, here?
result = await promise.Task;
buffer.Mapping = new RhiBuffer.ActiveMapping(buffer) { Valid = true };
}
if (result.Status != BufferMapAsyncStatus.Success)
throw new RhiException(result.Status.ToString());
buffer.MapState = RhiBufferMapState.Mapped;
}
internal override unsafe RhiMappedBufferRange BufferGetMappedRange(RhiBuffer buffer, nuint offset, nuint size)
{
if (size > int.MaxValue)
throw new ArgumentException("Mapped area too big!");
if (buffer.Mapping == null)
throw new InvalidOperationException("Buffer is not mapped");
lock (buffer.Mapping)
{
if (!buffer.Mapping.Valid)
{
// Not sure if this is possible, but can't hurt.
throw new InvalidOperationException();
}
var nativeBuffer = _bufferRegistry[buffer.Handle].Native;
var mapped = _webGpu.BufferGetMappedRange(nativeBuffer, offset, size);
return new RhiMappedBufferRange(buffer.Mapping, mapped, (int) size);
}
}
internal override unsafe void BufferUnmap(RhiBuffer buffer)
{
if (buffer.Mapping == null)
throw new InvalidOperationException("Buffer is not mapped!");
lock (buffer.Mapping)
{
if (!buffer.Mapping.Valid)
{
// Not sure if this is possible, but can't hurt.
throw new InvalidOperationException();
}
if (buffer.Mapping.ActiveSpans > 0)
throw new InvalidOperationException("Current thread has buffer accessible as span, cannot unmap!");
var nativeBuffer = _bufferRegistry[buffer.Handle].Native;
_webGpu.BufferUnmap(nativeBuffer);
buffer.Mapping.Valid = false;
buffer.Mapping = null;
buffer.MapState = RhiBufferMapState.Unmapped;
}
}
internal override unsafe void BufferDrop(RhiBuffer buffer)
{
_wgpu.BufferDrop(_bufferRegistry[buffer.Handle].Native);
_bufferRegistry.Remove(buffer.Handle);
}
private sealed unsafe class BufferReg
{
public Buffer* Native;
}
private record struct WgpuMapBufferAsyncResult(BufferMapAsyncStatus Status);
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static unsafe void WgpuMapBufferAsyncCallback(BufferMapAsyncStatus status, void* userdata)
{
WgpuPromise<WgpuMapBufferAsyncResult>.SetResult(userdata, new WgpuMapBufferAsyncResult(status));
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, CommandBufferReg> _commandBufferRegistry = new();
/// <summary>
/// Command buffer was dropped natively, either via explicit call or implicit side effect (e.g. queue submit).
/// </summary>
private void CommandBufferDropped(RhiCommandBuffer commandBuffer)
{
_commandBufferRegistry.Remove(commandBuffer.Handle);
GC.SuppressFinalize(commandBuffer);
}
internal override void CommandBufferDrop(RhiCommandBuffer commandBuffer)
{
_wgpu.CommandBufferDrop(_commandBufferRegistry[commandBuffer.Handle].Native);
CommandBufferDropped(commandBuffer);
}
}

View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using Silk.NET.WebGPU;
using Buffer = Silk.NET.WebGPU.Buffer;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, CommandEncoderReg> _commandEncoderRegistry = new();
private readonly Dictionary<RhiHandle, RenderPassEncoderReg> _renderPassEncoderRegistry = new();
public override RhiCommandEncoder CreateCommandEncoder(in RhiCommandEncoderDescriptor descriptor)
{
CommandEncoder* nativeEncoder;
fixed (byte* pLabel = MakeLabel(descriptor.Label))
{
var nativeDescriptor = new CommandEncoderDescriptor
{
Label = pLabel
};
nativeEncoder = _webGpu.DeviceCreateCommandEncoder(_wgpuDevice, &nativeDescriptor);
}
// TODO: thread safety
var handle = AllocRhiHandle();
_commandEncoderRegistry.Add(handle, new CommandEncoderReg { Native = nativeEncoder });
return new RhiCommandEncoder(this, handle);
}
internal override RhiRenderPassEncoder CommandEncoderBeginRenderPass(
RhiCommandEncoder encoder,
in RhiRenderPassDescriptor descriptor)
{
// TODO: Ensure not disposed
// TODO: Thread safety
Span<byte> buffer = stackalloc byte[512];
var pDescriptor = BumpAllocate<RenderPassDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
var colorAttachments = descriptor.ColorAttachments;
pDescriptor->ColorAttachmentCount = (uint)colorAttachments.Length;
pDescriptor->ColorAttachments = BumpAllocate<RenderPassColorAttachment>(ref buffer, colorAttachments.Length);
for (var i = 0; i < colorAttachments.Length; i++)
{
ref var attachment = ref colorAttachments[i];
var pAttachment = &pDescriptor->ColorAttachments[i];
pAttachment->View = _textureViewRegistry[attachment.View.Handle].Native;
if (attachment.ResolveTarget is { } resolveTarget)
pAttachment->ResolveTarget = _textureViewRegistry[resolveTarget.Handle].Native;
pAttachment->ClearValue = WgpuColor(attachment.ClearValue);
pAttachment->LoadOp = (LoadOp)attachment.LoadOp;
pAttachment->StoreOp = (StoreOp)attachment.StoreOp;
}
if (descriptor.DepthStencilAttachment is { } depthStencilAttachment)
{
var pDepthStencilAttachment = BumpAllocate<RenderPassDepthStencilAttachment>(ref buffer);
pDescriptor->DepthStencilAttachment = pDepthStencilAttachment;
pDepthStencilAttachment->View = _textureViewRegistry[depthStencilAttachment.View.Handle].Native;
pDepthStencilAttachment->DepthLoadOp = (LoadOp)depthStencilAttachment.DepthLoadOp;
pDepthStencilAttachment->DepthStoreOp = (StoreOp)depthStencilAttachment.DepthStoreOp;
pDepthStencilAttachment->DepthClearValue = depthStencilAttachment.DepthClearValue;
pDepthStencilAttachment->DepthReadOnly = depthStencilAttachment.DepthReadOnly;
pDepthStencilAttachment->StencilLoadOp = (LoadOp)depthStencilAttachment.StencilLoadOp;
pDepthStencilAttachment->StencilStoreOp = (StoreOp)depthStencilAttachment.StencilStoreOp;
pDepthStencilAttachment->StencilClearValue = depthStencilAttachment.StencilClearValue;
pDepthStencilAttachment->StencilReadOnly = depthStencilAttachment.StencilReadOnly;
}
if (descriptor.OcclusionQuerySet != null)
throw new NotImplementedException();
var pDescriptorMaxDrawCount = BumpAllocate<RenderPassDescriptorMaxDrawCount>(ref buffer);
pDescriptor->NextInChain = (ChainedStruct*)pDescriptorMaxDrawCount;
pDescriptorMaxDrawCount->Chain.SType = SType.RenderPassDescriptorMaxDrawCount;
pDescriptorMaxDrawCount->MaxDrawCount = descriptor.MaxDrawCount;
var nativeEncoder = _webGpu.CommandEncoderBeginRenderPass(
_commandEncoderRegistry[encoder.Handle].Native,
pDescriptor
);
// TODO: thread safety
var handle = AllocRhiHandle();
_renderPassEncoderRegistry.Add(handle, new RenderPassEncoderReg { Native = nativeEncoder });
return new RhiRenderPassEncoder(this, handle);
}
internal override void RenderPassEncoderSetPipeline(
in RhiRenderPassEncoder encoder,
RhiRenderPipeline pipeline)
{
// TODO: safety
_webGpu.RenderPassEncoderSetPipeline(
_renderPassEncoderRegistry[encoder.Handle].Native,
_renderPipelineRegistry[pipeline.Handle].Native
);
}
internal override void RenderPassEncoderDraw(
in RhiRenderPassEncoder encoder,
uint vertexCount,
uint instanceCount,
uint firstVertex,
uint firstInstance)
{
// TODO: safety
_webGpu.RenderPassEncoderDraw(
_renderPassEncoderRegistry[encoder.Handle].Native,
vertexCount,
instanceCount,
firstVertex,
firstInstance
);
}
internal override void RenderPassEncoderEnd(RhiRenderPassEncoder encoder)
{
// TODO: safety
var handle = encoder.Handle;
_webGpu.RenderPassEncoderEnd(_renderPassEncoderRegistry[handle].Native);
RenderPassEncoderDropped(handle);
}
internal override void RenderPassEncoderSetBindGroup(
RhiRenderPassEncoder encoder,
uint index,
RhiBindGroup? bindGroup)
{
_webGpu.RenderPassEncoderSetBindGroup(
_renderPassEncoderRegistry[encoder.Handle].Native,
index,
_bindGroupRegistry[bindGroup!.Handle].Native,
0, null
);
}
internal override void RenderPassEncoderSetVertexBuffer(
RhiRenderPassEncoder encoder,
uint slot,
RhiBuffer? buffer,
ulong offset,
ulong? size)
{
Buffer* nativeBuffer = null;
if (buffer != null)
nativeBuffer = _bufferRegistry[buffer.Handle].Native;
_webGpu.RenderPassEncoderSetVertexBuffer(
_renderPassEncoderRegistry[encoder.Handle].Native,
slot,
nativeBuffer,
offset,
size ?? WebGPU.WholeSize
);
}
internal override void RenderPassEncoderSetScissorRect(
RhiRenderPassEncoder encoder,
uint x, uint y, uint w, uint h)
{
// TODO: safety
_webGpu.RenderPassEncoderSetScissorRect(
_renderPassEncoderRegistry[encoder.Handle].Native,
x,
y,
w,
h
);
}
internal override RhiCommandBuffer CommandEncoderFinish(
in RhiCommandEncoder encoder,
in RhiCommandBufferDescriptor descriptor)
{
// TODO: safety
var handle = encoder.Handle;
Span<byte> buffer = stackalloc byte[512];
var pDescriptor = BumpAllocate<CommandBufferDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
var nativeBuffer = _webGpu.CommandEncoderFinish(
_commandEncoderRegistry[handle].Native,
pDescriptor
);
CommandEncoderDropped(handle);
var bufferHandle = AllocRhiHandle();
_commandBufferRegistry.Add(bufferHandle, new CommandBufferReg { Native = nativeBuffer });
return new RhiCommandBuffer(this, bufferHandle);
}
private void CommandEncoderDropped(RhiHandle encoder)
{
_commandEncoderRegistry.Remove(encoder);
}
private void RenderPassEncoderDropped(RhiHandle encoder)
{
_renderPassEncoderRegistry.Remove(encoder);
}
private sealed class CommandEncoderReg
{
public CommandEncoder* Native;
}
private sealed class RenderPassEncoderReg
{
public RenderPassEncoder* Native;
}
private sealed class CommandBufferReg
{
public CommandBuffer* Native;
}
}

View File

@@ -0,0 +1,12 @@
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
// wgpu-native and Dawn can't agree on how resources should be dropped
// Dawn wants reference counting, wgpu doesn't.
// This will (in the future) abstract the two.
private void WgpuDropTextureView(TextureView* tv) => _wgpu.TextureViewDrop(tv);
}

View File

@@ -0,0 +1,239 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Shared.Maths;
using Silk.NET.WebGPU;
using Color = Silk.NET.WebGPU.Color;
using RColor = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private static string WgpuVersionToString(uint version)
{
var a = (version >> 24) & 0xFF;
var b = (version >> 16) & 0xFF;
var c = (version >> 08) & 0xFF;
var d = (version >> 00) & 0xFF;
return $"{a}.{b}.{c}.{d}";
}
private static Color WgpuColor(RhiColor color) => new()
{
R = color.R,
G = color.G,
B = color.B,
A = color.A
};
private static Extent3D WgpuExtent3D(RhiExtent3D extent)
{
return new Extent3D
{
Width = extent.Width,
Height = extent.Height,
DepthOrArrayLayers = extent.Depth
};
}
private static Origin3D WgpuOrigin3D(RhiOrigin3D origin)
{
return new Origin3D
{
X = origin.X,
Y = origin.Y,
Z = origin.Z
};
}
private static RhiTextureFormat ValidateTextureFormat(RhiTextureFormat format)
{
if (format is 0 or >= RhiTextureFormat.Final)
throw new ArgumentException($"Invalid {nameof(RhiTextureFormat)}");
return format;
}
private static RhiTextureDimension ValidateTextureDimension(RhiTextureDimension dimension)
{
if (dimension > RhiTextureDimension.Dim3D)
throw new ArgumentException($"Invalid {nameof(RhiTextureDimension)}");
return dimension;
}
private static RhiTextureUsage ValidateTextureUsage(RhiTextureUsage usage)
{
if (usage >= RhiTextureUsage.Final)
throw new ArgumentException($"Invalid {nameof(RhiTextureUsage)}");
return usage;
}
private static RhiTextureViewDimension ValidateTextureViewDimension(RhiTextureViewDimension dimension)
{
if (dimension >= RhiTextureViewDimension.Final)
throw new ArgumentException($"Invalid {nameof(RhiTextureViewDimension)}");
return dimension;
}
private static RhiTextureAspect ValidateTextureAspect(RhiTextureAspect aspect)
{
if (aspect >= RhiTextureAspect.Final)
throw new ArgumentException($"Invalid {nameof(RhiTextureAspect)}");
return aspect;
}
private static RhiAddressMode ValidateAddressMode(RhiAddressMode addressMode)
{
if (addressMode >= RhiAddressMode.Final)
throw new ArgumentException($"Invalid {nameof(RhiAddressMode)}");
return addressMode;
}
private static RhiFilterMode ValidateFilterMode(RhiFilterMode filterMode)
{
if (filterMode >= RhiFilterMode.Final)
throw new ArgumentException($"Invalid {nameof(RhiFilterMode)}");
return filterMode;
}
private static RhiMipmapFilterMode ValidateMipmapFilterMode(RhiMipmapFilterMode mipmapFilterMode)
{
if (mipmapFilterMode >= RhiMipmapFilterMode.Final)
throw new ArgumentException($"Invalid {nameof(RhiMipmapFilterMode)}");
return mipmapFilterMode;
}
private static RhiCompareFunction ValidateCompareFunction(RhiCompareFunction compareFunction)
{
if (compareFunction >= RhiCompareFunction.Final)
throw new ArgumentException($"Invalid {nameof(RhiCompareFunction)}");
return compareFunction;
}
private static PowerPreference ValidatePowerPreference(RhiPowerPreference powerPreference)
{
if (powerPreference >= RhiPowerPreference.Final)
throw new ArgumentException($"Invalid {nameof(RhiPowerPreference)}");
return (PowerPreference) powerPreference;
}
private static string MarshalFromString(byte* str)
{
return Marshal.PtrToStringUTF8((nint)str)!;
}
[return: NotNullIfNotNull(nameof(label))]
private static byte[]? MakeLabel(string? label)
{
// TODO: Replace with stackalloc
if (label == null)
return null;
return Encoding.UTF8.GetBytes(label);
}
/// <param name="buf">Must be pinned memory or I WILL COME TO YOUR HOUSE!!</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void* BumpAllocate(ref Span<byte> buf, int size)
{
// Round up to 8 to make sure everything stays aligned inside.
var alignedSize = MathHelper.CeilingPowerOfTwo(size, 8);
if (buf.Length < alignedSize)
ThrowBumpAllocOutOfSpace();
var ptr = Unsafe.AsPointer(ref MemoryMarshal.AsRef<byte>(buf));
buf = buf[alignedSize..];
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T* BumpAllocate<T>(ref Span<byte> buf) where T : unmanaged
{
var ptr = (T*)BumpAllocate(ref buf, sizeof(T));
// Yeah I don't trust myself.
*ptr = default;
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T* BumpAllocate<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T*)ptr;
}
// Workaround for C# not having pointers in generics.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T** BumpAllocatePtr<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T*) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T**)ptr;
}
private static byte* BumpAllocateUtf8(ref Span<byte> buf, string? str)
{
if (str == null)
return null;
var byteCount = Encoding.UTF8.GetByteCount(str) + 1;
var ptr = BumpAllocate(ref buf, byteCount);
var dstSpan = new Span<byte>(ptr, byteCount);
Encoding.UTF8.GetBytes(str, dstSpan);
dstSpan[^1] = 0;
return (byte*) ptr;
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowBumpAllocOutOfSpace()
{
throw new InvalidOperationException("Out of bump allocator space!");
}
private sealed class WgpuPromise<TResult> : IDisposable
{
private readonly TaskCompletionSource<TResult> _tcs;
private GCHandle _gcHandle;
public Task<TResult> Task => _tcs.Task;
public void* UserData => (void*) GCHandle.ToIntPtr(_gcHandle);
public WgpuPromise()
{
_tcs = new TaskCompletionSource<TResult>();
_gcHandle = GCHandle.Alloc(this);
}
public static void SetResult(void* userdata, TResult result)
{
var self = (WgpuPromise<TResult>)GCHandle.FromIntPtr((nint) userdata).Target!;
self._tcs.SetResult(result);
}
public void Dispose()
{
_gcHandle.Free();
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
public override RhiQueue Queue { get; }
// queue is ignored as parameter, since WebGPU only supports one queue for now.
internal override void QueueWriteTexture(
RhiQueue queue,
in RhiImageCopyTexture destination,
ReadOnlySpan<byte> data,
in RhiImageDataLayout dataLayout,
RhiExtent3D size)
{
// TODO: Thread safety
var nativeTexture = _textureRegistry[destination.Texture.Handle].Native;
var nativeDestination = new ImageCopyTexture
{
Aspect = (TextureAspect)ValidateTextureAspect(destination.Aspect),
Texture = nativeTexture,
Origin = WgpuOrigin3D(destination.Origin),
MipLevel = destination.MipLevel
};
var nativeDataLayout = new TextureDataLayout
{
// TODO: Validation
Offset = dataLayout.Offset,
BytesPerRow = dataLayout.BytesPerRow,
RowsPerImage = dataLayout.RowsPerImage
};
var nativeSize = WgpuExtent3D(size);
fixed (byte* pData = data)
{
_webGpu.QueueWriteTexture(
_wgpuQueue,
&nativeDestination,
pData, (nuint) data.Length,
&nativeDataLayout,
&nativeSize
);
}
}
public override void QueueWriteBuffer(RhiBuffer buffer, ulong bufferOffset, ReadOnlySpan<byte> data)
{
var nativeBuffer = _bufferRegistry[buffer.Handle].Native;
fixed (byte* pData = data)
{
_webGpu.QueueWriteBuffer(
_wgpuQueue,
nativeBuffer,
bufferOffset,
pData,
(nuint) data.Length);
}
}
internal override void QueueSubmit(RhiQueue queue, RhiCommandBuffer[] commandBuffers)
{
// TODO: Safety
var pBuffers = stackalloc CommandBuffer*[commandBuffers.Length];
for (var i = 0; i < commandBuffers.Length; i++)
{
pBuffers[i] = _commandBufferRegistry[commandBuffers[i].Handle].Native;
}
_webGpu.QueueSubmit(
_wgpuQueue,
(uint) commandBuffers.Length,
pBuffers
);
foreach (var commandBuffer in commandBuffers)
{
CommandBufferDropped(commandBuffer);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed partial class RhiWebGpu
{
private long _rhiHandleCounter;
private RhiHandle AllocRhiHandle() => new(Interlocked.Increment(ref _rhiHandleCounter));
}

View File

@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, RenderPipelineReg> _renderPipelineRegistry = new();
private readonly Dictionary<RhiHandle, PipelineLayoutReg> _pipelineLayoutRegistry = new();
public override RhiPipelineLayout CreatePipelineLayout(in RhiPipelineLayoutDescriptor descriptor)
{
// TODO: SAFETY
Span<byte> buffer = stackalloc byte[128];
var pDescriptor = BumpAllocate<PipelineLayoutDescriptor>(ref buffer);
pDescriptor->Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
var layouts = descriptor.BindGroupLayouts;
pDescriptor->BindGroupLayoutCount = (uint) layouts.Length;
pDescriptor->BindGroupLayouts = BumpAllocatePtr<BindGroupLayout>(ref buffer, layouts.Length);
for (var i = 0; i < layouts.Length; i++)
{
pDescriptor->BindGroupLayouts[i] = _bindGroupLayoutRegistry[layouts[i].Handle].Native;
}
var native = _webGpu.DeviceCreatePipelineLayout(_wgpuDevice, pDescriptor);
// TODO: Thread safety
var handle = AllocRhiHandle();
_pipelineLayoutRegistry.Add(handle, new PipelineLayoutReg { Native = native });
return new RhiPipelineLayout(this, handle);
}
public override RhiRenderPipeline CreateRenderPipeline(in RhiRenderPipelineDescriptor descriptor)
{
// TODO: THREAD SAFETY
// TODO: INPUT VALIDATION
var vertexShader = _shaderModuleRegistry[descriptor.Vertex.ProgrammableStage.ShaderModule.Handle].Native;
const int bufferSize = 8192;
var bufferPtr = NativeMemory.AlignedAlloc(bufferSize, 8);
RenderPipeline* nativePipeline;
try
{
var buffer = new Span<byte>(bufferPtr, bufferSize);
RenderPipelineDescriptor pipelineDesc = default;
pipelineDesc.Label = BumpAllocateUtf8(ref buffer, descriptor.Label);
// Pipeline layout
switch (descriptor.Layout)
{
case RhiPipelineLayout pipelineLayout:
pipelineDesc.Layout = _pipelineLayoutRegistry[pipelineLayout.Handle].Native;
break;
case RhiAutoLayoutMode:
throw new NotSupportedException("wgpu does not support auto layout yet");
// Default case: no layout given, do nothing
}
// Vertex state
pipelineDesc.Vertex.Module = vertexShader;
pipelineDesc.Vertex.EntryPoint = BumpAllocateUtf8(
ref buffer,
descriptor.Vertex.ProgrammableStage.EntryPoint);
WgpuProgrammableConstants(
ref buffer,
descriptor.Vertex.ProgrammableStage.Constants,
out pipelineDesc.Vertex.ConstantCount,
out pipelineDesc.Vertex.Constants);
var buffers = descriptor.Vertex.Buffers;
pipelineDesc.Vertex.BufferCount = (uint)buffers.Length;
pipelineDesc.Vertex.Buffers = BumpAllocate<VertexBufferLayout>(ref buffer, buffers.Length);
for (var i = 0; i < buffers.Length; i++)
{
ref var bufferLayout = ref pipelineDesc.Vertex.Buffers[i];
bufferLayout.ArrayStride = buffers[i].ArrayStride;
bufferLayout.StepMode = (VertexStepMode)buffers[i].StepMode;
var attributes = buffers[i].Attributes;
bufferLayout.AttributeCount = (uint)attributes.Length;
bufferLayout.Attributes = BumpAllocate<VertexAttribute>(ref buffer, attributes.Length);
for (var j = 0; j < attributes.Length; j++)
{
ref var attribute = ref bufferLayout.Attributes[j];
attribute.Format = (VertexFormat)attributes[j].Format;
attribute.Offset = attributes[j].Offset;
attribute.ShaderLocation = attributes[j].ShaderLocation;
}
}
// Primitive state
pipelineDesc.Primitive.Topology = (PrimitiveTopology)descriptor.Primitive.Topology;
pipelineDesc.Primitive.StripIndexFormat = (IndexFormat)descriptor.Primitive.StripIndexformat;
pipelineDesc.Primitive.FrontFace = (FrontFace)descriptor.Primitive.FrontFace;
pipelineDesc.Primitive.CullMode = (CullMode)descriptor.Primitive.CullMode;
var pPrimitiveDepthClipControl = BumpAllocate<PrimitiveDepthClipControl>(ref buffer);
pPrimitiveDepthClipControl->Chain.SType = SType.PrimitiveDepthClipControl;
pipelineDesc.Primitive.NextInChain = &pPrimitiveDepthClipControl->Chain;
pPrimitiveDepthClipControl->UnclippedDepth = descriptor.Primitive.UnclippedDepth;
// Depth stencil state
if (descriptor.DepthStencil is { } depthStencil)
{
var pDepthStencil = BumpAllocate<DepthStencilState>(ref buffer);
pipelineDesc.DepthStencil = pDepthStencil;
pDepthStencil->Format = (TextureFormat)depthStencil.Format;
pDepthStencil->DepthWriteEnabled = depthStencil.DepthWriteEnabled;
pDepthStencil->DepthCompare = (CompareFunction)depthStencil.DepthCompare;
pDepthStencil->StencilFront = WgpuStencilFaceState(depthStencil.StencilFront ?? new RhiStencilFaceState());
pDepthStencil->StencilBack = WgpuStencilFaceState(depthStencil.StencilBack ?? new RhiStencilFaceState());
pDepthStencil->StencilReadMask = depthStencil.StencilReadMask;
pDepthStencil->StencilWriteMask = depthStencil.StencilWriteMask;
pDepthStencil->DepthBias = depthStencil.DepthBias;
pDepthStencil->DepthBiasSlopeScale = depthStencil.DepthBiasSlopeScale;
pDepthStencil->DepthBiasClamp = depthStencil.DepthBiasClamp;
}
// Multisample state
pipelineDesc.Multisample.Count = descriptor.Multisample.Count;
pipelineDesc.Multisample.Mask = descriptor.Multisample.Mask;
pipelineDesc.Multisample.AlphaToCoverageEnabled = descriptor.Multisample.AlphaToCoverageEnabled;
// Fragment state
if (descriptor.Fragment is { } fragment)
{
var fragmentShader = _shaderModuleRegistry[fragment.ProgrammableStage.ShaderModule.Handle].Native;
var pFragment = BumpAllocate<FragmentState>(ref buffer);
pipelineDesc.Fragment = pFragment;
pFragment->Module = fragmentShader;
pFragment->EntryPoint = BumpAllocateUtf8(ref buffer, fragment.ProgrammableStage.EntryPoint);
WgpuProgrammableConstants(
ref buffer,
fragment.ProgrammableStage.Constants,
out pFragment->ConstantCount,
out pFragment->Constants);
var targets = fragment.Targets;
pFragment->TargetCount = (uint)targets.Length;
pFragment->Targets = BumpAllocate<ColorTargetState>(ref buffer, targets.Length);
for (var i = 0; i < targets.Length; i++)
{
ref var target = ref pFragment->Targets[i];
target.Format = (TextureFormat)targets[i].Format;
if (targets[i].Blend is { } blend)
{
var pBlend = BumpAllocate<BlendState>(ref buffer);
target.Blend = pBlend;
pBlend->Alpha = WgpuBlendComponent(blend.Alpha);
pBlend->Color = WgpuBlendComponent(blend.Color);
}
target.WriteMask = (ColorWriteMask)targets[i].WriteMask;
}
}
nativePipeline = _webGpu.DeviceCreateRenderPipeline(_wgpuDevice, &pipelineDesc);
}
finally
{
NativeMemory.AlignedFree(bufferPtr);
}
// TODO: Thread safety
var handle = AllocRhiHandle();
_renderPipelineRegistry.Add(handle, new RenderPipelineReg { Native = nativePipeline });
return new RhiRenderPipeline(this, handle);
}
private static StencilFaceState WgpuStencilFaceState(in RhiStencilFaceState state)
{
return new StencilFaceState
{
Compare = (CompareFunction)state.Compare,
FailOp = (StencilOperation)state.FailOp,
DepthFailOp = (StencilOperation)state.DepthFailOp,
PassOp = (StencilOperation)state.PassOp
};
}
private static void WgpuProgrammableConstants(
ref Span<byte> buffer,
RhiConstantEntry[] constants,
out uint constantCount,
out ConstantEntry* pConstants)
{
constantCount = (uint)constants.Length;
pConstants = BumpAllocate<ConstantEntry>(ref buffer, constants.Length);
for (var i = 0; i < constants.Length; i++)
{
ref var constant = ref pConstants[i];
constant.Key = BumpAllocateUtf8(ref buffer, constants[i].Key);
constant.Value = constants[i].Value;
}
}
private static BlendComponent WgpuBlendComponent(in RhiBlendComponent component)
{
return new BlendComponent
{
Operation = (BlendOperation)component.Operation,
DstFactor = (BlendFactor)component.DstFactor,
SrcFactor = (BlendFactor)component.SrcFactor,
};
}
private sealed class RenderPipelineReg
{
public RenderPipeline* Native;
}
private sealed class PipelineLayoutReg
{
public PipelineLayout* Native;
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, SamplerReg> _samplerRegistry = new();
public override RhiSampler CreateSampler(in RhiSamplerDescriptor descriptor)
{
var addressModeU = ValidateAddressMode(descriptor.AddressModeU);
var addressModeV = ValidateAddressMode(descriptor.AddressModeV);
var addressModeW = ValidateAddressMode(descriptor.AddressModeW);
var magFilter = ValidateFilterMode(descriptor.MagFilter);
var minFilter = ValidateFilterMode(descriptor.MinFilter);
var mipmapFilter = ValidateMipmapFilterMode(descriptor.MipmapFilter);
var compare = ValidateCompareFunction(descriptor.Compare);
Sampler* sampler;
fixed (byte* label = MakeLabel(descriptor.Label))
{
var samplerDesc = new SamplerDescriptor
{
AddressModeU = (AddressMode) addressModeU,
AddressModeV = (AddressMode) addressModeV,
AddressModeW = (AddressMode) addressModeW,
MagFilter = (FilterMode) magFilter,
MinFilter = (FilterMode) minFilter,
MipmapFilter = (MipmapFilterMode) mipmapFilter,
LodMinClamp = descriptor.LodMinClamp,
LodMaxClamp = descriptor.LodMaxClamp,
Compare = (CompareFunction) compare,
MaxAnisotropy = descriptor.MaxAnisotropy,
Label = label
};
sampler = _webGpu.DeviceCreateSampler(_wgpuDevice, &samplerDesc);
}
// TODO: Thread safety
var handle = AllocRhiHandle();
_samplerRegistry.Add(handle, new SamplerReg { Native = sampler });
return new RhiSampler(this, handle);
}
private sealed class SamplerReg
{
public Sampler* Native;
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Text;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, ShaderModuleReg> _shaderModuleRegistry = new();
public override RhiShaderModule CreateShaderModule(in RhiShaderModuleDescriptor descriptor)
{
var codeBytes = Encoding.UTF8.GetBytes(descriptor.Code);
ShaderModule* shaderModule;
fixed (byte* pCode = codeBytes)
fixed (byte* pLabel = MakeLabel(descriptor.Label))
{
var descWgsl = new ShaderModuleWGSLDescriptor();
descWgsl.Code = pCode;
descWgsl.Chain.SType = SType.ShaderModuleWgsldescriptor;
var desc = new ShaderModuleDescriptor();
desc.Label = pLabel;
desc.NextInChain = (ChainedStruct*) (&descWgsl);
shaderModule = _webGpu.DeviceCreateShaderModule(_wgpuDevice, &desc);
}
// TODO: Thread safety
var handle = AllocRhiHandle();
_shaderModuleRegistry.Add(handle, new ShaderModuleReg { Native = shaderModule });
return new RhiShaderModule(this, handle);
}
private sealed class ShaderModuleReg
{
public ShaderModule* Native;
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Utility;
using Silk.NET.WebGPU;
using WGPUTexture = Silk.NET.WebGPU.Texture;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
private readonly Dictionary<RhiHandle, TextureReg> _textureRegistry = new();
private readonly Dictionary<RhiHandle, TextureViewReg> _textureViewRegistry = new();
public override RhiTexture CreateTexture(in RhiTextureDescriptor descriptor)
{
var format = descriptor.Format;
var dimension = descriptor.Dimension;
var usage = descriptor.Usage;
ValidateTextureFormat(format);
ValidateTextureDimension(dimension);
ValidateTextureUsage(usage);
// TODO: Copy to stackalloc instead.
var viewFormats = descriptor.ViewFormats?.ToArray() ?? Array.Empty<RhiTextureFormat>();
foreach (var vf in viewFormats)
{
ValidateTextureFormat(vf);
}
DebugTools.Assert(
sizeof(RhiTextureFormat) == sizeof(TextureFormat),
"Pointer to view formats array is cast directly to pass to native, sizes must match");
WGPUTexture* texturePtr;
fixed (byte* label = MakeLabel(descriptor.Label))
fixed (RhiTextureFormat* pViewFormats = viewFormats)
{
var webGpuDesc = new TextureDescriptor
{
SampleCount = descriptor.SampleCount,
MipLevelCount = descriptor.MipLevelCount,
Dimension = (TextureDimension) dimension,
Format = (TextureFormat) format,
Label = label,
Size = WgpuExtent3D(descriptor.Size),
Usage = (TextureUsage) usage,
ViewFormats = (TextureFormat*) pViewFormats,
ViewFormatCount = checked((uint) viewFormats.Length),
};
texturePtr = _webGpu.DeviceCreateTexture(_wgpuDevice, &webGpuDesc);
}
if (texturePtr == null)
throw new RhiException("Texture creation failed");
// TODO: Thread safety
var handle = AllocRhiHandle();
_textureRegistry.Add(handle, new TextureReg { Native = texturePtr });
return new RhiTexture(this, handle);
}
internal override RhiTextureView TextureCreateView(RhiTexture texture, in RhiTextureViewDescriptor descriptor)
{
// TODO: Thread safety
var nativeTexture = _textureRegistry[texture.Handle].Native;
var format = ValidateTextureFormat(descriptor.Format);
var dimension = ValidateTextureViewDimension(descriptor.Dimension);
var aspect = ValidateTextureAspect(descriptor.Aspect);
var mipLevelCount = descriptor.MipLevelCount;
var arrayLayerCount = descriptor.ArrayLayerCount;
if (mipLevelCount == 0)
throw new ArgumentException($"Invalid {nameof(descriptor.MipLevelCount)}");
if (arrayLayerCount == 0)
throw new ArgumentException($"Invalid {nameof(descriptor.ArrayLayerCount)}");
TextureView* textureView;
fixed (byte* label = MakeLabel(descriptor.Label))
{
var webGpuDesc = new TextureViewDescriptor
{
Format = (TextureFormat) format,
Dimension = (TextureViewDimension) dimension,
Aspect = (TextureAspect) aspect,
Label = label,
BaseMipLevel = descriptor.BaseMipLevel,
MipLevelCount = mipLevelCount,
BaseArrayLayer = descriptor.BaseArrayLayer,
ArrayLayerCount = descriptor.ArrayLayerCount
};
textureView = _webGpu.TextureCreateView(nativeTexture, &webGpuDesc);
}
return AllocRhiTextureView(textureView);
}
internal override void TextureViewDrop(RhiTextureView textureView)
{
_wgpu.TextureViewDrop(_textureViewRegistry[textureView.Handle].Native);
_textureViewRegistry.Remove(textureView.Handle);
}
internal override RhiTextureView CreateTextureViewForWindow(Clyde.WindowReg reg)
{
// TODO: Thread safety
var swapChain = reg.RhiWebGpuData!.SwapChain;
// This creates a new texture view handle.
var textureView = _webGpu.SwapChainGetCurrentTextureView(swapChain);
return AllocRhiTextureView(textureView);
}
private RhiTextureView AllocRhiTextureView(TextureView* native)
{
// TODO: Thread safety
var handle = AllocRhiHandle();
_textureViewRegistry.Add(handle, new TextureViewReg { Native = native });
return new RhiTextureView(this, handle);
}
private sealed class TextureReg
{
public WGPUTexture* Native;
}
private sealed class TextureViewReg
{
public TextureView* Native;
}
}

View File

@@ -0,0 +1,146 @@
using System;
using Robust.Shared.Utility;
using Silk.NET.WebGPU;
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu
{
public sealed class WindowData
{
public Surface* Surface;
public SwapChain* SwapChain;
}
private void CreateSurfaceForWindow(Clyde.WindowReg window)
{
DebugTools.Assert(_clyde._windowing != null);
SurfaceDescriptor surfaceDesc = default;
SurfaceDescriptorFromWindowsHWND surfaceDescHwnd;
SurfaceDescriptorFromXlibWindow surfaceDescX11;
SurfaceDescriptorFromMetalLayer surfaceDescMetal;
if (OperatingSystem.IsWindows())
{
var hInstance = _clyde._windowing.WindowGetWin32Instance(window);
var hWnd = _clyde._windowing.WindowGetWin32Window(window);
surfaceDescHwnd = new SurfaceDescriptorFromWindowsHWND
{
Chain =
{
SType = SType.SurfaceDescriptorFromWindowsHwnd
},
Hinstance = hInstance,
Hwnd = hWnd
};
surfaceDesc.NextInChain = (ChainedStruct*) (&surfaceDescHwnd);
}
else
{
var xDisplay = _clyde._windowing.WindowGetX11Display(window);
var xWindow = _clyde._windowing.WindowGetX11Id(window);
if (xDisplay != null && xWindow != null)
{
surfaceDescX11 = new SurfaceDescriptorFromXlibWindow
{
Chain =
{
SType = SType.SurfaceDescriptorFromXlibWindow
},
Display = ((IntPtr) xDisplay.Value).ToPointer(),
Window = xWindow.Value
};
surfaceDesc.NextInChain = (ChainedStruct*) (&surfaceDescX11);
}
else
{
var metalLayer = _clyde._windowing.WindowGetMetalLayer(window);
if (metalLayer != null)
{
surfaceDescMetal = new SurfaceDescriptorFromMetalLayer
{
Chain =
{
SType = SType.SurfaceDescriptorFromMetalLayer
},
Layer = ((IntPtr) metalLayer.Value).ToPointer()
};
surfaceDesc.NextInChain = (ChainedStruct*) (&surfaceDescMetal);
}
else
{
throw new NotImplementedException();
}
}
}
var surface = _webGpu.InstanceCreateSurface(_wgpuInstance, &surfaceDesc);
window.RhiWebGpuData = new WindowData
{
Surface = surface
};
}
private void CreateSwapChainForWindow(Clyde.WindowReg window)
{
// TODO: Safety
var rhiData = window.RhiWebGpuData!;
var format = TextureFormat.Bgra8UnormSrgb;
_sawmill.Debug($"Preferred surface format is {format}");
var swapChainDesc = new SwapChainDescriptor
{
Format = format,
Height = (uint)window.FramebufferSize.Y,
Width = (uint)window.FramebufferSize.X,
Usage = TextureUsage.RenderAttachment,
PresentMode = PresentMode.Fifo
};
var swapChain = _webGpu.DeviceCreateSwapChain(_wgpuDevice, rhiData.Surface, &swapChainDesc);
rhiData.SwapChain = swapChain;
_sawmill.Debug("WebGPU Surface created!");
}
internal override void WindowCreated(Clyde.WindowReg reg)
{
CreateSurfaceForWindow(reg);
CreateSwapChainForWindow(reg);
}
internal override void WindowDestroy(Clyde.WindowReg reg)
{
var rhiData = reg.RhiWebGpuData!;
_wgpu.SwapChainDrop(rhiData.SwapChain);
_wgpu.SurfaceDrop(rhiData.Surface);
reg.RhiWebGpuData = null;
}
internal override void WindowRecreateSwapchain(Clyde.WindowReg reg)
{
var rhiData = reg.RhiWebGpuData!;
_wgpu.SwapChainDrop(rhiData.SwapChain);
CreateSwapChainForWindow(reg);
}
internal override void WindowPresent(Clyde.WindowReg reg)
{
// TODO: Safety
var rhiData = reg.RhiWebGpuData!;
_webGpu.SwapChainPresent(rhiData.SwapChain);
}
}

View File

@@ -0,0 +1,356 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Silk.NET.WebGPU;
using Silk.NET.WebGPU.Extensions.WGPU;
using LogLevel = Silk.NET.WebGPU.Extensions.WGPU.LogLevel;
using RLogLevel = Robust.Shared.Log.LogLevel;
using RColor = Robust.Shared.Maths.Color;
#pragma warning disable CS8500
namespace Robust.Client.Graphics.Clyde.Rhi;
internal sealed unsafe partial class RhiWebGpu : RhiBase
{
private readonly Clyde _clyde;
private readonly IConfigurationManager _cfg;
private readonly ISawmill _sawmill;
private readonly ISawmill _apiLogSawmill;
private WebGPU _webGpu = default!;
private Wgpu _wgpu = default!;
private Instance* _wgpuInstance;
private Adapter* _wgpuAdapter;
private Device* _wgpuDevice;
private Queue* _wgpuQueue;
private RhiLimits? _deviceLimits;
private RhiAdapterProperties? _adapterProperties;
private string _description = "not initialized";
public override RhiLimits DeviceLimits =>
_deviceLimits ?? throw new InvalidOperationException("Not initialized yet");
public override RhiAdapterProperties AdapterProperties =>
_adapterProperties ?? throw new InvalidOperationException("Not initialized yet");
public override string Description => _description;
public RhiWebGpu(Clyde clyde, IDependencyCollection dependencies)
{
var logMgr = dependencies.Resolve<ILogManager>();
_cfg = dependencies.Resolve<IConfigurationManager>();
_clyde = clyde;
_sawmill = logMgr.GetSawmill("clyde.rhi.webGpu");
_apiLogSawmill = logMgr.GetSawmill("clyde.rhi.webGpu.apiLog");
Queue = new RhiQueue(this, default);
}
internal override void Init()
{
_sawmill.Info("Initializing WebGPU RHI!");
InitInstance();
CreateSurfaceForWindow(_clyde._mainWindow!);
_sawmill.Debug("WebGPU main surface created!");
InitAdapterAndDevice(_clyde._mainWindow!.RhiWebGpuData!.Surface);
CreateSwapChainForWindow(_clyde._mainWindow!);
}
private void InitInstance()
{
var context = WebGPU.CreateDefaultContext(new[]
{ "wgpu_native.dll", "libwgpu_native.so", "libwgpu_native.dylib" });
_webGpu = new WebGPU(context);
_wgpu = new Wgpu(context);
var wgpuVersion = WgpuVersionToString(_wgpu.GetVersion());
_sawmill.Debug($"wgpu-native loaded, version: {wgpuVersion}");
_description = $"WebGPU (wgpu-native {wgpuVersion})";
InitLogging();
Span<byte> buffer = stackalloc byte[128];
var pInstanceDescriptor = BumpAllocate<InstanceDescriptor>(ref buffer);
// Specify instance extras for wgpu-native.
var pInstanceExtras = BumpAllocate<InstanceExtras>(ref buffer);
pInstanceDescriptor->NextInChain = (ChainedStruct*)pInstanceExtras;
pInstanceExtras->Chain.SType = (SType)NativeSType.STypeInstanceExtras;
pInstanceExtras->Backends = (uint)GetInstanceBackendCfg();
_wgpuInstance = _webGpu.CreateInstance(pInstanceDescriptor);
_sawmill.Debug("WebGPU instance created!");
}
private InstanceBackend GetInstanceBackendCfg()
{
var configured = _cfg.GetCVar(CVars.DisplayWgpuBackends);
if (configured == "all")
return InstanceBackend.Primary | InstanceBackend.Secondary;
var backends = InstanceBackend.None;
foreach (var opt in configured.Split(","))
{
backends |= opt switch
{
"vulkan" => InstanceBackend.Vulkan,
"gl" => InstanceBackend.GL,
"metal" => InstanceBackend.Metal,
"dx12" => InstanceBackend.DX12,
"dx11" => InstanceBackend.DX11,
"browser" => InstanceBackend.BrowserWebGpu,
_ => throw new ArgumentException($"Unknown wgpu backend: '{opt}'")
};
}
return backends;
}
private void InitAdapterAndDevice(Surface* forSurface)
{
var powerPreference = ValidatePowerPreference(
(RhiPowerPreference)_cfg.GetCVar(CVars.DisplayGpuPowerPreference)
);
var requestAdapterOptions = new RequestAdapterOptions
{
CompatibleSurface = forSurface,
PowerPreference = powerPreference
};
WgpuRequestAdapterResult result;
_webGpu.InstanceRequestAdapter(
_wgpuInstance,
&requestAdapterOptions,
new PfnRequestAdapterCallback(&WgpuRequestAdapterCallback),
&result);
if (result.Status != RequestAdapterStatus.Success)
throw new RhiException($"Adapter request failed: {result.Message}");
_sawmill.Debug("WebGPU adapter created!");
_wgpuAdapter = result.Adapter.P;
AdapterProperties adapterProps = default;
_webGpu.AdapterGetProperties(_wgpuAdapter, &adapterProps);
SupportedLimits adapterLimits = default;
_webGpu.AdapterGetLimits(_wgpuAdapter, &adapterLimits);
_sawmill.Debug($"adapter name: {MarshalFromString(adapterProps.Name)}");
_sawmill.Debug($"adapter vendor: {MarshalFromString(adapterProps.VendorName)} ({adapterProps.VendorID})");
_sawmill.Debug($"adapter driver: {MarshalFromString(adapterProps.DriverDescription)}");
_sawmill.Debug($"adapter architecture: {MarshalFromString(adapterProps.Architecture)}");
_sawmill.Debug($"adapter backend: {adapterProps.BackendType}");
_sawmill.Debug($"adapter type: {adapterProps.AdapterType}");
_sawmill.Debug($"adapter UBO alignment: {adapterLimits.Limits.MinUniformBufferOffsetAlignment}");
_adapterProperties = new RhiAdapterProperties(
adapterProps.VendorID,
MarshalFromString(adapterProps.VendorName),
MarshalFromString(adapterProps.Architecture),
MarshalFromString(adapterProps.Name),
MarshalFromString(adapterProps.DriverDescription),
(RhiAdapterType) adapterProps.AdapterType,
(RhiBackendType) adapterProps.BackendType
);
// Default limits, from WebGPU spec.
var requiredLimits = new RequiredLimits();
if (false)
{
// GLES3.0
requiredLimits.Limits.MaxComputeWorkgroupStorageSize = 16384;
requiredLimits.Limits.MaxComputeInvocationsPerWorkgroup = 256;
requiredLimits.Limits.MaxComputeWorkgroupSizeX = 256;
requiredLimits.Limits.MaxComputeWorkgroupSizeY = 256;
requiredLimits.Limits.MaxComputeWorkgroupSizeZ = 256;
requiredLimits.Limits.MaxComputeWorkgroupsPerDimension = 65536;
requiredLimits.Limits.MaxDynamicStorageBuffersPerPipelineLayout = 0;
requiredLimits.Limits.MaxStorageBuffersPerShaderStage = 4;
requiredLimits.Limits.MaxStorageBufferBindingSize = 134217728;
}
// Required minimums
requiredLimits.Limits.MinStorageBufferOffsetAlignment = 256;
requiredLimits.Limits.MinUniformBufferOffsetAlignment = 256;
requiredLimits.Limits.MaxTextureDimension1D = 8192;
requiredLimits.Limits.MaxTextureDimension2D = 8192;
requiredLimits.Limits.MaxTextureDimension3D = 2048;
requiredLimits.Limits.MaxTextureArrayLayers = 256;
requiredLimits.Limits.MaxBindGroups = 4;
requiredLimits.Limits.MaxBindingsPerBindGroup = 1000;
requiredLimits.Limits.MaxDynamicUniformBuffersPerPipelineLayout = 8;
requiredLimits.Limits.MaxSampledTexturesPerShaderStage = 16;
requiredLimits.Limits.MaxSamplersPerShaderStage = 16;
requiredLimits.Limits.MaxUniformBuffersPerShaderStage = 12;
requiredLimits.Limits.MaxUniformBufferBindingSize = 65536;
requiredLimits.Limits.MaxVertexBuffers = 8;
requiredLimits.Limits.MaxVertexAttributes = 16;
requiredLimits.Limits.MaxVertexBufferArrayStride = 2048;
requiredLimits.Limits.MaxInterStageShaderComponents = 60;
requiredLimits.Limits.MaxInterStageShaderVariables = 16;
requiredLimits.Limits.MaxColorAttachments = 8;
requiredLimits.Limits.MaxColorAttachmentBytesPerSample = 32;
requiredLimits.Limits.MaxBufferSize = 268435456;
// Custom limits
// Take as low UBO alignment as we can get.
requiredLimits.Limits.MinUniformBufferOffsetAlignment = adapterLimits.Limits.MinUniformBufferOffsetAlignment;
var deviceDesc = new DeviceDescriptor();
deviceDesc.RequiredLimits = &requiredLimits;
WgpuRequestDeviceResult deviceResult;
_webGpu.AdapterRequestDevice(
_wgpuAdapter,
&deviceDesc,
new PfnRequestDeviceCallback(&WgpuRequestDeviceCallback),
&deviceResult);
if (deviceResult.Status != RequestDeviceStatus.Success)
throw new Exception($"Device request failed: {deviceResult.Message}");
_sawmill.Debug("WebGPU device created!");
_wgpuDevice = deviceResult.Device;
_wgpuQueue = _webGpu.DeviceGetQueue(_wgpuDevice);
ref var limits = ref requiredLimits.Limits;
_deviceLimits = new RhiLimits(
limits.MaxTextureDimension1D,
limits.MaxTextureDimension2D,
limits.MaxTextureDimension3D,
limits.MaxTextureArrayLayers,
limits.MaxBindGroups,
limits.MaxBindingsPerBindGroup,
limits.MaxDynamicUniformBuffersPerPipelineLayout,
limits.MaxDynamicStorageBuffersPerPipelineLayout,
limits.MaxSampledTexturesPerShaderStage,
limits.MaxSamplersPerShaderStage,
limits.MaxStorageBuffersPerShaderStage,
limits.MaxStorageTexturesPerShaderStage,
limits.MaxUniformBuffersPerShaderStage,
limits.MaxUniformBufferBindingSize,
limits.MaxStorageBufferBindingSize,
limits.MinUniformBufferOffsetAlignment,
limits.MinStorageBufferOffsetAlignment,
limits.MaxVertexBuffers,
limits.MaxBufferSize,
limits.MaxVertexAttributes,
limits.MaxVertexBufferArrayStride,
limits.MaxInterStageShaderComponents,
limits.MaxInterStageShaderVariables,
limits.MaxColorAttachments,
limits.MaxColorAttachmentBytesPerSample,
limits.MaxComputeWorkgroupStorageSize,
limits.MaxComputeInvocationsPerWorkgroup,
limits.MaxComputeWorkgroupSizeX,
limits.MaxComputeWorkgroupSizeY,
limits.MaxComputeWorkgroupSizeZ,
limits.MaxComputeWorkgroupsPerDimension
);
InitErrorCallback();
}
private void InitLogging()
{
// TODO: clear this.
var gcHandle = GCHandle.Alloc(this);
_wgpu.SetLogCallback(new PfnLogCallback(&LogCallback), (void*)GCHandle.ToIntPtr(gcHandle));
_wgpu.SetLogLevel(LogLevel.Warn);
}
private void InitErrorCallback()
{
// TODO: clear this.
var gcHandle = GCHandle.Alloc(this);
_webGpu.DeviceSetUncapturedErrorCallback(
_wgpuDevice,
new PfnErrorCallback(&UncapturedErrorCallback),
(void*)GCHandle.ToIntPtr(gcHandle));
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void LogCallback(LogLevel level, byte* message, void* userdata)
{
var self = (RhiWebGpu)GCHandle.FromIntPtr((nint)userdata).Target!;
var messageString = Marshal.PtrToStringUTF8((nint)message)!;
var robustLevel = level switch
{
LogLevel.Error => RLogLevel.Error,
LogLevel.Warn => RLogLevel.Warning,
LogLevel.Info => RLogLevel.Info,
LogLevel.Debug => RLogLevel.Debug,
LogLevel.Trace or _ => RLogLevel.Verbose,
};
self._apiLogSawmill.Log(robustLevel, messageString);
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void UncapturedErrorCallback(ErrorType level, byte* message, void* userdata)
{
var self = (RhiWebGpu)GCHandle.FromIntPtr((nint)userdata).Target!;
var messageString = Marshal.PtrToStringUTF8((nint)message)!;
self._apiLogSawmill.Error(messageString);
}
internal override void Shutdown()
{
}
private record struct WgpuRequestAdapterResult(RequestAdapterStatus Status, Ptr<Adapter> Adapter, string Message);
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void WgpuRequestAdapterCallback(
RequestAdapterStatus status,
Adapter* adapter,
byte* message,
void* userdata)
{
*(WgpuRequestAdapterResult*)userdata = new WgpuRequestAdapterResult(
status,
adapter,
MarshalFromString(message));
}
private record struct WgpuRequestDeviceResult(RequestDeviceStatus Status, Ptr<Device> Device, string Message);
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void WgpuRequestDeviceCallback(
RequestDeviceStatus status,
Device* device,
byte* message,
void* userdata)
{
*(WgpuRequestDeviceResult*)userdata = new WgpuRequestDeviceResult(
status,
device,
MarshalFromString(message));
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace Robust.Client.Graphics.Clyde;
/// <summary>
/// Simple C-style preprocessor for WGSL.
/// </summary>
public static partial class ShaderParser2
{
private static readonly Regex RegexInclude = MyRegex();
public static string Magic(string source, Func<string, string> resolveInclude)
{
var writer = new StringWriter();
Process(writer, source, resolveInclude);
return writer.ToString();
}
private static void Process(StringWriter writer, string source, Func<string, string> resolveInclude)
{
var reader = new StringReader(source);
while (reader.ReadLine() is { } line)
{
var match = RegexInclude.Match(line);
if (match.Success)
{
var name = match.Groups[1].Value;
var included = resolveInclude(name);
Process(writer, included, resolveInclude);
continue;
}
writer.WriteLine(line);
}
}
[GeneratedRegex("^\\s*#\\s*include\\s*<\\s*([^>]+)\\s*>")]
private static partial Regex MyRegex();
}

View File

@@ -1,16 +0,0 @@
#ifndef HAS_VARYING_ATTRIBUTE
#define texture2D texture
#define varying in
#define attribute in
#define gl_FragColor colourOutput
out highp vec4 colourOutput;
#endif
varying highp vec2 UV;
uniform sampler2D tex;
void main()
{
gl_FragColor = texture2D(tex, UV);
}

View File

@@ -1,20 +0,0 @@
#ifndef HAS_VARYING_ATTRIBUTE
#define texture2D texture
#define varying out
#define attribute in
#endif
// Vertex position.
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
varying vec2 UV;
void main()
{
UV = tCoord;
gl_Position = vec4(aPos, 0.0, 1.0);
}

View File

@@ -264,7 +264,7 @@ namespace Robust.Client.Graphics.Clyde
{
// As far as I can tell, sometimes entering fullscreen just disables vsync.
// Hilarious!
_clyde._glContext?.UpdateVSync();
// _clyde._glContext?.UpdateVSync();
}
}
}

View File

@@ -253,9 +253,7 @@ namespace Robust.Client.Graphics.Clyde
) : CmdBase;
private sealed record CmdWinCreate(
GLContextSpec? GLSpec,
WindowCreateParameters Parameters,
nint ShareWindow,
nint OwnerWindow,
TaskCompletionSource<GlfwWindowCreateResult> Tcs
) : CmdBase;

View File

@@ -240,41 +240,40 @@ namespace Robust.Client.Graphics.Clyde
}
}
public nint? WindowGetWin32Window(WindowReg window)
// GLFW doesn't have the Metal view/layer API that SDL2 does.
// It might be possible though.
public nint? WindowGetMetalLayer(WindowReg window) => null;
public HWND WindowGetWin32Window(WindowReg window)
{
if (!OperatingSystem.IsWindows())
return null;
return default;
var reg = (GlfwWindowReg) window;
try
{
return GLFW.GetWin32Window(reg.GlfwWindow);
return (HWND) GLFW.GetWin32Window(reg.GlfwWindow);
}
catch (EntryPointNotFoundException)
{
return null;
return default;
}
}
public HINSTANCE WindowGetWin32Instance(WindowReg window)
{
throw new NotImplementedException();
}
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner)
{
Window* sharePtr = null;
if (share is GlfwWindowReg glfwReg)
sharePtr = glfwReg.GlfwWindow;
Window* ownerPtr = null;
if (owner is GlfwWindowReg glfwOwnerReg)
ownerPtr = glfwOwnerReg.GlfwWindow;
var task = SharedWindowCreate(
spec,
parameters,
sharePtr,
ownerPtr);
var task = SharedWindowCreate(parameters, ownerPtr);
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
@@ -308,10 +307,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private Task<GlfwWindowCreateResult> SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
Window* share, Window* owner)
private Task<GlfwWindowCreateResult> SharedWindowCreate(WindowCreateParameters parameters, Window* owner)
{
//
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
@@ -331,9 +327,7 @@ namespace Robust.Client.Graphics.Clyde
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<GlfwWindowCreateResult>();
SendCmd(new CmdWinCreate(
glSpec,
parameters,
(nint) share,
(nint) owner,
tcs));
@@ -349,9 +343,9 @@ namespace Robust.Client.Graphics.Clyde
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (glSpec, parameters, share, owner, tcs) = cmd;
var (parameters, owner, tcs) = cmd;
var window = CreateGlfwWindowForRenderer(glSpec, parameters, (Window*) share, (Window*) owner);
var window = CreateGlfwWindowForRenderer(parameters, (Window*) owner);
if (window == null)
{
@@ -394,57 +388,14 @@ namespace Robust.Client.Graphics.Clyde
}
private Window* CreateGlfwWindowForRenderer(
GLContextSpec? spec,
WindowCreateParameters parameters,
Window* contextShare,
Window* ownerWindow)
{
GLFW.WindowHint(WindowHintString.X11ClassName, "RobustToolbox");
GLFW.WindowHint(WindowHintString.X11InstanceName, "RobustToolbox");
GLFW.WindowHint(WindowHintBool.ScaleToMonitor, true);
if (spec == null)
{
// No OpenGL context requested.
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi);
}
else
{
var s = spec.Value;
#if DEBUG
GLFW.WindowHint(WindowHintBool.OpenGLDebugContext, true);
#endif
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, s.Major);
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, s.Minor);
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, s.Profile != GLContextProfile.Compatibility);
GLFW.WindowHint(WindowHintBool.SrgbCapable, true);
switch (s.Profile)
{
case GLContextProfile.Compatibility:
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
break;
case GLContextProfile.Core:
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
break;
case GLContextProfile.Es:
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlEsApi);
break;
}
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi,
s.CreationApi == GLContextCreationApi.Egl
? ContextApi.EglContextApi
: ContextApi.NativeContextApi);
if (s.CreationApi == GLContextCreationApi.Egl)
WsiShared.EnsureEglAvailable();
}
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi);
Monitor* monitor = null;
if (parameters.Monitor != null &&
@@ -474,7 +425,7 @@ namespace Robust.Client.Graphics.Clyde
parameters.Width, parameters.Height,
parameters.Title,
parameters.Fullscreen ? monitor : null,
contextShare);
null);
// Check if window failed to create.
if (window == null)
@@ -692,32 +643,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void GLMakeContextCurrent(WindowReg? window)
{
if (window != null)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg)window;
GLFW.MakeContextCurrent(reg.GlfwWindow);
}
else
{
GLFW.MakeContextCurrent(null);
}
}
public void GLSwapInterval(WindowReg reg, int interval)
{
GLFW.SwapInterval(interval);
}
public void* GLGetProcAddress(string procName)
{
return (void*) GLFW.GetProcAddress(procName);
}
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
// Not supported on GLFW.

View File

@@ -4,12 +4,13 @@ using Robust.Client.Input;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics.Clyde
{
partial class Clyde
{
private interface IWindowingImpl
internal interface IWindowingImpl
{
// Lifecycle stuff
bool Init();
@@ -31,9 +32,7 @@ namespace Robust.Client.Graphics.Clyde
// Window API.
(WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner);
void WindowDestroy(WindowReg reg);
@@ -45,7 +44,9 @@ namespace Robust.Client.Graphics.Clyde
void WindowSwapBuffers(WindowReg window);
uint? WindowGetX11Id(WindowReg window);
nint? WindowGetX11Display(WindowReg window);
nint? WindowGetWin32Window(WindowReg window);
nint? WindowGetMetalLayer(WindowReg window);
HWND WindowGetWin32Window(WindowReg window);
HINSTANCE WindowGetWin32Instance(WindowReg window);
// Keyboard
string? KeyGetName(Keyboard.Key key);
@@ -56,12 +57,6 @@ namespace Robust.Client.Graphics.Clyde
void UpdateMainWindowMode();
// OpenGL-related stuff.
// Note: you should probably go through GLContextBase instead, which calls these functions.
void GLMakeContextCurrent(WindowReg? reg);
void GLSwapInterval(WindowReg reg, int interval);
unsafe void* GLGetProcAddress(string procName);
// Misc
void RunOnWindowThread(Action a);

View File

@@ -221,10 +221,7 @@ internal partial class Clyde
private sealed class CmdWinCreate : CmdBase
{
public required GLContextSpec? GLSpec;
public required WindowCreateParameters Parameters;
public required nint ShareWindow;
public required nint ShareContext;
public required nint OwnerWindow;
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
}
@@ -232,6 +229,7 @@ internal partial class Clyde
private sealed class CmdWinDestroy : CmdBase
{
public nint Window;
public nint MetalView;
public bool HadOwner;
}
@@ -240,7 +238,6 @@ internal partial class Clyde
public Sdl3WindowReg? Reg;
public string? Error;
}
private sealed class CmdRunAction : CmdBase
{
public required Action Action;

View File

@@ -10,7 +10,6 @@ using Robust.Shared.Utility;
using SharpFont;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics
{
@@ -114,7 +113,7 @@ namespace Robust.Client.Graphics
if (bitmap.Pitch != 0)
{
Image<A8> img;
Image<Rgba32> img;
switch (bitmap.PixelMode)
{
case PixelMode.Mono:
@@ -131,13 +130,13 @@ namespace Robust.Client.Graphics
span = new ReadOnlySpan<A8>((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows);
}
img = new Image<A8>(bitmap.Width, bitmap.Rows);
img = new Image<Rgba32>(bitmap.Width, bitmap.Rows);
span.Blit(
BlitA8ToRgba32(
span,
bitmap.Pitch,
UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows),
img,
(0, 0));
img);
break;
}
@@ -199,14 +198,14 @@ namespace Robust.Client.Graphics
OwnedTexture GenSheet()
{
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
var sheet = _clyde.CreateBlankTexture<Rgba32>((SheetWidth, SheetHeight),
$"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}");
scaled.AtlasTextures.Add(sheet);
return sheet;
}
}
private static Image<A8> MonoBitMapToImage(FTBitmap bitmap)
private static Image<Rgba32> MonoBitMapToImage(FTBitmap bitmap)
{
DebugTools.Assert(bitmap.PixelMode == PixelMode.Mono);
DebugTools.Assert(bitmap.Pitch > 0);
@@ -217,7 +216,7 @@ namespace Robust.Client.Graphics
span = new ReadOnlySpan<byte>((void*) bitmap.Buffer, bitmap.Rows * bitmap.Pitch);
}
var bitmapImage = new Image<A8>(bitmap.Width, bitmap.Rows);
var bitmapImage = new Image<Rgba32>(bitmap.Width, bitmap.Rows);
for (var y = 0; y < bitmap.Rows; y++)
{
for (var x = 0; x < bitmap.Width; x++)
@@ -226,13 +225,39 @@ namespace Robust.Client.Graphics
var bitIndex = x % 8;
var bit = (span[byteIndex] & (1 << (7 - bitIndex))) != 0;
bitmapImage[x, y] = new A8(bit ? byte.MaxValue : byte.MinValue);
bitmapImage[x, y] = new Rgba32(255, 255, 255, bit ? byte.MaxValue : byte.MinValue);
}
}
return bitmapImage;
}
private static void BlitA8ToRgba32(
ReadOnlySpan<A8> source,
int sourceWidth,
UIBox2i sourceRect,
Image<Rgba32> destination)
{
var dstSpan = ImageOps.GetPixelSpan(destination);
var dstWidth = destination.Width;
var srcHeight = sourceRect.Height;
var srcWidth = sourceRect.Width;
for (var y = 0; y < srcHeight; y++)
{
var sourceRowOffset = sourceWidth * (y + sourceRect.Top) + sourceRect.Left;
var destRowOffset = dstWidth * y;
var srcRow = source[sourceRowOffset..(sourceRowOffset + srcWidth)];
var dstRow = dstSpan[destRowOffset..(destRowOffset + srcWidth)];
for (var i = 0; i < srcRow.Length; i++)
{
dstRow[i] = new Rgba32(255, 255, 255, srcRow[i].PackedValue);
}
}
}
private sealed class FontFaceHandle : IFontFaceHandle
{
public Face Face { get; }

View File

@@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -17,6 +18,8 @@ namespace Robust.Client.Graphics
public interface IClyde
{
RhiBase Rhi { get; }
IClydeWindow MainWindow { get; }
IRenderTarget MainWindowRenderTarget => MainWindow.RenderTarget;

View File

@@ -2,12 +2,6 @@ namespace Robust.Client.Graphics
{
internal interface IClydeDebugInfo
{
OpenGLVersion OpenGLVersion { get; }
string Renderer { get; }
string Vendor { get; }
string VersionString { get; }
bool Overriding { get; }
string WindowingApi { get; }
}
}

View File

@@ -32,12 +32,10 @@ public abstract class Texture : IRsiStateLike
/// </summary>
public Vector2i Size { get; /*protected set;*/ }
public Color this[int x, int y] => this.GetPixel(x, y);
protected Texture(Vector2i size)
{
Size = size;
}
protected Texture(Vector2i size)
{
Size = size;
}
Texture IDirectionalTextureProvider.Default => this;

View File

@@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared;
@@ -216,7 +214,7 @@ namespace Robust.Client.ResourceManagement
// The array must be sorted from biggest to smallest first.
Array.Sort(atlasList, (b, a) => a.AtlasSheet.Height.CompareTo(b.AtlasSheet.Height));
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
var maxSize = _configurationManager.GetCVar(CVars.ResRSIAtlasSize);
// THIS IS NOT GUARANTEED TO HAVE ANY PARTICULARLY LOGICAL ORDERING.
// E.G you could have atlas 1 RSIs appear *before* you're done seeing atlas 2 RSIs.

View File

@@ -13,11 +13,12 @@
<PackageReference Include="DiscordRichPresence" PrivateAssets="compile" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" PrivateAssets="compile" />
<PackageReference Include="Silk.NET.WebGPU" PrivateAssets="compile" />
<PackageReference Include="Silk.NET.WebGPU.Extensions.WGPU" PrivateAssets="compile" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3" Condition="'$(UseSystemSqlite)' == 'True'" PrivateAssets="compile" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Condition="'$(UseSystemSqlite)' != 'True'" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.NFluidsynth" PrivateAssets="compile" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="OpenToolkit.Graphics" PrivateAssets="compile" />
<PackageReference Include="OpenTK.Audio.OpenAL" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.SharpFont" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sdl" PrivateAssets="compile" />

View File

@@ -40,15 +40,14 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
_textBuilder.Clear();
var rhi = _clydeInternal.Rhi;
var info = _clydeInternal.DebugInfo;
var stats = _clydeInternal.DebugStats;
_textBuilder.AppendLine($@"Renderer: {info.Renderer}
Vendor: {info.Vendor}
Version: {info.VersionString}");
if (info.Overriding)
_textBuilder.Append($"Version override: {info.OpenGLVersion}\n");
_textBuilder.AppendLine($@"RHI: {rhi.Description}
Adapter: {rhi.AdapterProperties.Name}
Vendor: {rhi.AdapterProperties.VendorName} ({rhi.AdapterProperties.VendorID})
Driver: {rhi.AdapterProperties.Driver}");
_textBuilder.Append($"Windowing: {info.WindowingApi}\n");

View File

@@ -7,5 +7,6 @@
<DevWindowTabPerf Name="Perf" />
<DevWindowTabTextures Name="Textures" />
<DevWindowTabRenderTargets Name="RenderTargets" />
<DevWindowTabRenderer Name="Renderer" />
</TabContainer>
</Control>

View File

@@ -29,6 +29,7 @@ namespace Robust.Client.UserInterface
TabContainer.SetTabTitle(Perf, "Profiling");
TabContainer.SetTabTitle(Textures, Loc.GetString("dev-window-tab-textures-title"));
TabContainer.SetTabTitle(RenderTargets, Loc.GetString("dev-window-tab-render-targets-title"));
TabContainer.SetTabTitle(Renderer, "Renderer");
Stylesheet =
new DefaultStylesheet(IoCManager.Resolve<IResourceCache>(), IoCManager.Resolve<IUserInterfaceManager>()).Stylesheet;

View File

@@ -0,0 +1,12 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.DevWindowTabRenderer">
<TabContainer>
<ScrollContainer Name="TabProperties">
<GridContainer Name="PropertiesGrid" Columns="2" Margin="2" />
</ScrollContainer>
<ScrollContainer Name="TabLimits">
<GridContainer Name="LimitsGrid" Columns="2" Margin="2" />
</ScrollContainer>
</TabContainer>
</Control>

View File

@@ -0,0 +1,90 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde.Rhi;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
[GenerateTypedNameReferences]
public sealed partial class DevWindowTabRenderer : Control
{
public DevWindowTabRenderer()
{
RobustXamlLoader.Load(this);
var clyde = IoCManager.Resolve<IClyde>();
var rhi = clyde.Rhi;
LoadLimits(rhi.DeviceLimits);
LoadProperties(rhi);
TabContainer.SetTabTitle(TabProperties, "Properties");
TabContainer.SetTabTitle(TabLimits, "Limits");
}
private void LoadProperties(RhiBase rhi)
{
Add("RHI description", rhi.Description);
var adapter = rhi.AdapterProperties;
Add("Adapter name", adapter.Name);
Add("Adapter vendor", $"{adapter.VendorName} ({adapter.VendorID})");
Add("Adapter driver", $"{adapter.Driver}");
Add("Adapter architecture", $"{adapter.Architecture}");
Add("Adapter backend", $"{adapter.BackendType}");
Add("Adapter type", $"{adapter.AdapterType}");
void Add<T>(string name, T field) where T : notnull
{
PropertiesGrid.AddChild(new Label { Text = name, Margin = new Thickness(0, 0, 8, 0) });
PropertiesGrid.AddChild(new Label { Text = field.ToString() });
}
}
private void LoadLimits(RhiLimits limits)
{
// @formatter:off
Add("MaxTextureDimension1D", limits.MaxTextureDimension1D);
Add("MaxTextureDimension2D", limits.MaxTextureDimension2D);
Add("MaxTextureDimension3D", limits.MaxTextureDimension3D);
Add("MaxTextureArrayLayers", limits.MaxTextureArrayLayers);
Add("MaxBindGroups", limits.MaxBindGroups);
Add("MaxBindingsPerBindGroup", limits.MaxBindingsPerBindGroup);
Add("MaxDynamicUniformBuffersPerPipelineLayout", limits.MaxDynamicUniformBuffersPerPipelineLayout);
Add("MaxDynamicStorageBuffersPerPipelineLayout", limits.MaxDynamicStorageBuffersPerPipelineLayout);
Add("MaxSampledTexturesPerShaderStage", limits.MaxSampledTexturesPerShaderStage);
Add("MaxSamplersPerShaderStage", limits.MaxSamplersPerShaderStage);
Add("MaxStorageBuffersPerShaderStage", limits.MaxStorageBuffersPerShaderStage);
Add("MaxStorageTexturesPerShaderStage", limits.MaxStorageTexturesPerShaderStage);
Add("MaxUniformBuffersPerShaderStage", limits.MaxUniformBuffersPerShaderStage);
Add("MaxUniformBufferBindingSize", limits.MaxUniformBufferBindingSize);
Add("MaxStorageBufferBindingSize", limits.MaxStorageBufferBindingSize);
Add("MinUniformBufferOffsetAlignment", limits.MinUniformBufferOffsetAlignment);
Add("MinStorageBufferOffsetAlignment", limits.MinStorageBufferOffsetAlignment);
Add("MaxVertexBuffers", limits.MaxVertexBuffers);
Add("MaxBufferSize", limits.MaxBufferSize);
Add("MaxVertexAttributes", limits.MaxVertexAttributes);
Add("MaxVertexBufferArrayStride", limits.MaxVertexBufferArrayStride);
Add("MaxInterStageShaderComponents", limits.MaxInterStageShaderComponents);
Add("MaxInterStageShaderVariables", limits.MaxInterStageShaderVariables);
Add("MaxColorAttachments", limits.MaxColorAttachments);
Add("MaxColorAttachmentBytesPerSample", limits.MaxColorAttachmentBytesPerSample);
Add("MaxComputeWorkgroupStorageSize", limits.MaxComputeWorkgroupStorageSize);
Add("MaxComputeInvocationsPerWorkgroup", limits.MaxComputeInvocationsPerWorkgroup);
Add("MaxComputeWorkgroupSizeX", limits.MaxComputeWorkgroupSizeX);
Add("MaxComputeWorkgroupSizeY", limits.MaxComputeWorkgroupSizeY);
Add("MaxComputeWorkgroupSizeZ", limits.MaxComputeWorkgroupSizeZ);
Add("MaxComputeWorkgroupsPerDimension", limits.MaxComputeWorkgroupsPerDimension);
// @formatter:on
void Add<T>(string name, T field) where T : notnull
{
LimitsGrid.AddChild(new Label { Text = name, Margin = new Thickness(0, 0, 8, 0) });
LimitsGrid.AddChild(new Label { Text = field.ToString() });
}
}
}

View File

@@ -26,7 +26,7 @@ internal sealed partial class UserInterfaceManager
if (control is WindowRoot root)
{
control.Measure(root.Window.RenderTarget.Size / root.UIScale);
control.Measure(root.Window.Size / root.UIScale);
}
else if (control.PreviousMeasure.HasValue)
{
@@ -46,7 +46,7 @@ internal sealed partial class UserInterfaceManager
if (control is WindowRoot root)
{
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.RenderTarget.Size / root.UIScale));
control.Arrange(UIBox2.FromDimensions(Vector2.Zero, root.Window.Size / root.UIScale));
}
else if (control.PreviousArrange.HasValue)
{
@@ -155,7 +155,10 @@ internal sealed partial class UserInterfaceManager
using (_prof.Group("Main"))
{
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
renderHandle.RenderInRenderTarget(_clyde.MainWindow.RenderTarget, () =>
{
DoRender(_windowsToRoot[_clyde.MainWindow.Id]);
}, Color.Pink);
}
void DoRender(WindowRoot root)

View File

@@ -102,8 +102,7 @@ internal partial class UserInterfaceManager
//Grab the OS UIScale or the value set through CVAR debug
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
var windowSize = root.Window.RenderTarget.Size;
var windowSize = root.Window.Size;
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
if (!_autoScaleEnabled || root.DisableAutoScaling || windowSize.X <= 0 || windowSize.Y <= 0)
return osScale;

View File

@@ -1,12 +0,0 @@
using Robust.Shared.Maths;
namespace Robust.Client.Utility
{
internal static class OpenTKConversions
{
public static OpenToolkit.Mathematics.Color4 ConvertOpenTK(this Color color)
{
return new(color.R, color.G, color.B, color.A);
}
}
}

View File

@@ -13,6 +13,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -759,6 +760,39 @@ namespace Robust.Shared.Maths
return true;
}
/// <summary>
/// Rounds <paramref name="value"/> up to a multiple of <paramref name="power"/>,
/// where <paramref name="power"/> is a power of two.
/// </summary>
/// <param name="value">The value to round up.</param>
/// <param name="power">The value to round up to a multiple of. Must be a power of two.</param>
/// <typeparam name="T">The type of integer to operate on.</typeparam>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T CeilingPowerOfTwo<T>(T value, T power)
where T : IBinaryInteger<T>
{
Debug.Assert((power & (power - T.One)) == T.Zero, "Power must be a power of two");
return (value + (power - T.One)) & ~(power - T.One);
}
// Non-generic function has less overhead without compiler optimizations.
/// <summary>
/// Rounds <paramref name="value"/> up to a multiple of <paramref name="power"/>,
/// where <paramref name="power"/> is a power of two.
/// </summary>
/// <param name="value">The value to round up.</param>
/// <param name="power">The value to round up to a multiple of. Must be a power of two.</param>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CeilingPowerOfTwo(int value, int power)
{
Debug.Assert(BitOperations.IsPow2(power), "Power must be a power of two");
return (value + (power - 1)) & ~(power - 1);
}
#endregion Public Members
}
}

View File

@@ -1037,136 +1037,12 @@ namespace Robust.Shared
// Clyde related enums are in Clyde.Constants.cs.
/// <summary>
/// Which renderer to use to render the game.
/// </summary>
public static readonly CVarDef<int> DisplayRenderer =
CVarDef.Create("display.renderer", 0, CVar.CLIENTONLY);
/// <summary>
/// Whether to use compatibility mode.
/// </summary>
/// <remarks>
/// This can change certain behaviors like GL version selection to try to avoid driver crashes/bugs.
/// </remarks>
public static readonly CVarDef<bool> DisplayCompat =
CVarDef.Create("display.compat", false, CVar.CLIENTONLY);
/// <summary>
/// Which OpenGL version to use for the OpenGL renderer.
/// Values correspond to the (private) RendererOpenGLVersion enum in Clyde.
/// </summary>
public static readonly CVarDef<int> DisplayOpenGLVersion =
CVarDef.Create("display.opengl_version", 0, CVar.CLIENTONLY);
/// <summary>
/// On Windows, use ANGLE as OpenGL implementation.
/// </summary>
public static readonly CVarDef<bool> DisplayAngle =
CVarDef.Create("display.angle", false, CVar.CLIENTONLY);
/// <summary>
/// Use a custom DXGI swap chain when using ANGLE.
/// Should improve performance and fixes main window sRGB handling with ANGLE.
/// </summary>
public static readonly CVarDef<bool> DisplayAngleCustomSwapChain =
CVarDef.Create("display.angle_custom_swap_chain", true, CVar.CLIENTONLY);
/// <summary>
/// Force ANGLE to create a GLES2 context (not a compatible GLES3 context).
/// </summary>
public static readonly CVarDef<bool> DisplayAngleForceEs2 =
CVarDef.Create("display.angle_force_es2", false, CVar.CLIENTONLY);
/// <summary>
/// Force ANGLE to create a context from a D3D11 FL 10_0 device.
/// </summary>
public static readonly CVarDef<bool> DisplayAngleForce10_0 =
CVarDef.Create("display.angle_force_10_0", false, CVar.CLIENTONLY);
/// <summary>
/// Force usage of DXGI 1.1 when using custom swap chain setup.
/// </summary>
public static readonly CVarDef<bool> DisplayAngleDxgi1 =
CVarDef.Create("display.angle_dxgi1", false, CVar.CLIENTONLY);
/// <summary>
/// Try to use the display adapter with this name, if the current renderer supports selecting it.
/// </summary>
public static readonly CVarDef<string> DisplayAdapter =
CVarDef.Create("display.adapter", "", CVar.CLIENTONLY);
/// <summary>
/// What type of GPU to prefer when creating a graphics context, for things such as hybrid GPU laptops.
/// </summary>
/// <remarks>
/// This setting is not always respect depending on platform and rendering API used.
/// Values are:
/// 0 = unspecified (DXGI_GPU_PREFERENCE_UNSPECIFIED)
/// 1 = minimum power (DXGI_GPU_PREFERENCE_MINIMUM_POWER)
/// 2 = high performance (DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE)
/// </remarks>
public static readonly CVarDef<int> DisplayGpuPreference =
CVarDef.Create("display.gpu_preference", 2, CVar.CLIENTONLY);
/// <summary>
/// Use EGL to create GL context instead of GLFW, if possible.
/// </summary>
/// <remarks>
/// This only tries to use EGL if on a platform like X11 or Windows (w/ ANGLE) where it is possible.
/// </remarks>
public static readonly CVarDef<bool> DisplayEgl =
CVarDef.Create("display.egl", false, CVar.CLIENTONLY);
/// <summary>
/// Enable allowES3OnFL10_0 on ANGLE.
/// </summary>
public static readonly CVarDef<bool> DisplayAngleEs3On10_0 =
CVarDef.Create("display.angle_es3_on_10_0", true, CVar.CLIENTONLY);
/// <summary>
/// Base DPI to render fonts at. This can be further scaled based on <c>display.uiScale</c>.
/// </summary>
public static readonly CVarDef<int> DisplayFontDpi =
CVarDef.Create("display.fontdpi", 96, CVar.CLIENTONLY);
/// <summary>
/// Override detected OpenGL version, for testing.
/// </summary>
public static readonly CVarDef<string> DisplayOGLOverrideVersion =
CVarDef.Create("display.ogl_override_version", string.Empty, CVar.CLIENTONLY);
/// <summary>
/// Run <c>glCheckError()</c> after (almost) every GL call.
/// </summary>
public static readonly CVarDef<bool> DisplayOGLCheckErrors =
CVarDef.Create("display.ogl_check_errors", false, CVar.CLIENTONLY);
/// <summary>
/// Forces synchronization of multi-window rendering with <c>glFinish</c> when GL fence sync is unavailable.
/// </summary>
/// <remarks>
/// If this is disabled multi-window rendering on GLES2 might run better, dunno.
/// It technically causes UB thanks to the OpenGL spec with cross-context sync. Hope that won't happen.
/// Let's be real the OpenGL specification is basically just a suggestion to drivers anyways so who cares.
/// </remarks>
public static readonly CVarDef<bool> DisplayForceSyncWindows =
CVarDef.Create<bool>("display.force_sync_windows", true, CVar.CLIENTONLY);
/// <summary>
/// Use a separate thread for multi-window blitting.
/// </summary>
public static readonly CVarDef<bool> DisplayThreadWindowBlit =
CVarDef.Create("display.thread_window_blit", true, CVar.CLIENTONLY);
/// <summary>
/// Diagnostic flag for testing. When using a separate thread for multi-window blitting,
/// should the worker be unblocked before the SwapBuffers(). Setting to true may improve
/// performance but may cause crashes or rendering errors.
/// </summary>
public static readonly CVarDef<bool> DisplayThreadUnlockBeforeSwap =
CVarDef.Create("display.thread_unlock_before_swap", false, CVar.CLIENTONLY);
/// <summary>
/// Buffer size of input command channel from windowing thread to main game thread.
/// </summary>
@@ -1219,6 +1095,37 @@ namespace Robust.Shared
public static readonly CVarDef<bool> DisplayThreadWindowApi =
CVarDef.Create("display.thread_window_api", false, CVar.CLIENTONLY);
public static readonly CVarDef<string> DisplayRhi =
CVarDef.Create("display.rhi", "webGpu", CVar.CLIENTONLY);
/// <summary>
/// Comma-separated list of backends to try to use, if using WebGPU RHI with wgpu.
/// </summary>
/// <remarks>
/// If set to "all", all backends will be tried.
/// </remarks>
public static readonly CVarDef<string> DisplayWgpuBackends =
CVarDef.Create("display.wgpu_backends", "all", CVar.CLIENTONLY);
/// <summary>
/// Try to use the display adapter with this name, if the current renderer supports selecting it.
/// </summary>
public static readonly CVarDef<string> DisplayAdapter =
CVarDef.Create("display.adapter", "", CVar.CLIENTONLY);
/// <summary>
/// What type of GPU to prefer when creating a graphics context, for things such as hybrid GPU laptops.
/// </summary>
/// <remarks>
/// This setting is not always respect depending on platform and rendering API used.
/// Values are:
/// 0 = unspecified
/// 1 = low power
/// 2 = high performance
/// </remarks>
public static readonly CVarDef<int> DisplayGpuPowerPreference =
CVarDef.Create("display.gpu_power_preference", 2, CVar.CLIENTONLY);
/*
* AUDIO
*/

View File

@@ -323,5 +323,27 @@ namespace Robust.UnitTesting.Shared.Utility
{
return MathHelper.CeilMultipleOfPowerOfTwo(value, powerOfTwo);
}
[Test]
[TestCase(5, 1, ExpectedResult = 5)]
[TestCase(1, 4, ExpectedResult = 4)]
[TestCase(4, 4, ExpectedResult = 4)]
[TestCase(5, 4, ExpectedResult = 8)]
[TestCase(19, 4, ExpectedResult = 20)]
public int TestCeilingPowerOfTwoInt(int value, int power)
{
return MathHelper.CeilingPowerOfTwo(value, power);
}
[Test]
[TestCase(5, 1, ExpectedResult = 5)]
[TestCase(1, 4, ExpectedResult = 4)]
[TestCase(4, 4, ExpectedResult = 4)]
[TestCase(5, 4, ExpectedResult = 8)]
[TestCase(19, 4, ExpectedResult = 20)]
public short TestCeilingPowerOfTwoGeneric(short value, short power)
{
return MathHelper.CeilingPowerOfTwo(value, power);
}
}
}