Add GPU acceleration for most of ApiHawk's GuiApi (gui.* lua APIs), refactor ApiHawk surfaces

This commit is contained in:
CasualPokePlayer 2024-05-24 13:25:28 -07:00
parent e1fe18be36
commit 476ac94d80
38 changed files with 1285 additions and 609 deletions

Binary file not shown.

BIN
Assets/dll/cimgui.dll Normal file

Binary file not shown.

BIN
Assets/dll/cimgui.so Normal file

Binary file not shown.

Binary file not shown.

View File

@ -3,6 +3,7 @@
<PackageVersion Include="Cyotek.Drawing.BitmapFont" Version="2.0.4" />
<PackageVersion Include="DotNetAnalyzers.DocumentationAnalyzers" Version="1.0.0-beta.59" />
<PackageVersion Include="Google.FlatBuffers" Version="23.5.26" /> <!-- last version with .NET Standard 2.0 support -->
<PackageVersion Include="ImGui.NET" Version="1.90.6.1" />
<PackageVersion Include="JunitXml.TestLogger" Version="3.0.124" />
<PackageVersion Include="Magick.NET-Q8-AnyCPU" Version="13.6.0" />
<PackageVersion Include="Menees.Analyzers" Version="3.0.10" />

View File

@ -9,7 +9,7 @@ project(SDL2-BizHawk C CXX)
# set(SDL_ATOMIC ON) # Used in HIDAPI (and disabling it just makes it emulate atomics rather than use intrinsics)
set(SDL_AUDIO OFF)
set(SDL_VIDEO ON) # Used in mupen64plus + OpenGL/D3D9 display methods
set(SDL_RENDER OFF)
set(SDL_RENDER ON) # We use the software renderer for GDI+ImGui 2D rendering
set(SDL_EVENTS ON) # Implied by SDL_VIDEO and SDL_JOYSTICK
set(SDL_JOYSTICK ON) # Used for our SDL2 input adapter
set(SDL_HAPTIC OFF)
@ -69,6 +69,7 @@ set(SDL_DIRECTFB OFF)
set(SDL_DUMMYVIDEO OFF)
set(SDL_RPI OFF)
set(SDL_RENDER_D3D OFF)
set(SDL_RENDER_METAL OFF)
set(SDL_VIVANTE OFF)
set(SDL_VULKAN OFF)
set(SDL_KMSDRM OFF)

View File

@ -41,7 +41,7 @@ namespace BizHawk.Bizware.Graphics.Controls
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
Context = new(Handle, 3, 0, true, false);
Context = new(Handle, 3, 2, true, false);
}
protected override void OnHandleDestroyed(EventArgs e)

View File

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cyotek.Drawing.BitmapFont" />
<PackageReference Include="ImGui.NET" ExcludeAssets="native" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="Silk.NET.OpenGL" />
<PackageReference Include="Silk.NET.WGL.Extensions.NV" />

View File

@ -31,6 +31,9 @@ namespace BizHawk.Bizware.Graphics
public ID3D11Buffer VertexBuffer;
public int VertexBufferCount;
public ID3D11Buffer IndexBuffer;
public int IndexBufferCount;
private readonly record struct D3D11Uniform(IntPtr VariablePointer, int VariableSize, D3D11PendingBuffer PB);
private readonly Dictionary<string, D3D11Uniform> _vsUniforms = new();
@ -99,7 +102,7 @@ namespace BizHawk.Bizware.Graphics
1 => Format.R32_Float,
2 => Format.R32G32_Float,
3 => Format.R32G32B32_Float,
4 => Format.R32G32B32A32_Float,
4 => item.Integer ? Format.B8G8R8A8_UNorm : Format.R32G32B32A32_Float,
_ => throw new InvalidOperationException()
};
@ -253,6 +256,9 @@ namespace BizHawk.Bizware.Graphics
VertexBuffer?.Dispose();
VertexBuffer = null;
VertexBufferCount = 0;
IndexBuffer?.Dispose();
IndexBuffer = null;
IndexBufferCount = 0;
for (var i = 0; i < ID3D11DeviceContext.CommonShaderConstantBufferSlotCount; i++)
{
@ -283,6 +289,62 @@ namespace BizHawk.Bizware.Graphics
}
}
public void SetVertexData(IntPtr data, int count)
{
if (VertexBufferCount < count)
{
VertexBuffer?.Dispose();
var bd = new BufferDescription( count * VertexStride, BindFlags.VertexBuffer, ResourceUsage.Dynamic, CpuAccessFlags.Write);
VertexBuffer = Device.CreateBuffer(in bd, data);
VertexBufferCount = count;
Context.IASetVertexBuffer(0, VertexBuffer, VertexStride);
}
else
{
var mappedVb = Context.Map(VertexBuffer, MapMode.WriteDiscard);
try
{
unsafe
{
Buffer.MemoryCopy((void*)data, (void*)mappedVb.DataPointer,
VertexBufferCount * VertexStride, count * VertexStride);
}
}
finally
{
Context.Unmap(VertexBuffer);
}
}
}
public void SetIndexData(IntPtr data, int count)
{
if (IndexBufferCount < count)
{
IndexBuffer?.Dispose();
var bd = new BufferDescription(count * 2, BindFlags.IndexBuffer, ResourceUsage.Dynamic, CpuAccessFlags.Write);
IndexBuffer = Device.CreateBuffer(in bd, data);
IndexBufferCount = count;
Context.IASetIndexBuffer(IndexBuffer, Format.R16_UInt, 0);
}
else
{
var mappedIb = Context.Map(IndexBuffer, MapMode.WriteDiscard);
try
{
unsafe
{
Buffer.MemoryCopy((void*)data, (void*)mappedIb.DataPointer,
IndexBufferCount * 2, count * 2);
}
}
finally
{
Context.Unmap(IndexBuffer);
}
}
}
public bool HasUniformSampler(string name)
{
return _vsSamplers.ContainsKey(name)
@ -299,6 +361,11 @@ namespace BizHawk.Bizware.Graphics
public void SetUniformSampler(string name, ITexture2D tex)
{
if (tex == null)
{
return;
}
var d3d11Tex = (D3D11Texture2D)tex;
var sampler = d3d11Tex.LinearFiltering ? LinearSamplerState : PointSamplerState;

View File

@ -81,7 +81,7 @@ namespace BizHawk.Bizware.Graphics
bd.RenderTarget[0].DestinationBlend = Blend.InverseSourceAlpha;
bd.RenderTarget[0].BlendOperation = BlendOperation.Add;
bd.RenderTarget[0].SourceBlendAlpha = Blend.One;
bd.RenderTarget[0].DestinationBlendAlpha = Blend.Zero;
bd.RenderTarget[0].DestinationBlendAlpha = Blend.InverseSourceAlpha;
bd.RenderTarget[0].BlendOperationAlpha = BlendOperation.Add;
bd.RenderTarget[0].RenderTargetWriteMask = ColorWriteEnable.All;
BlendEnableState = Device.CreateBlendState(bd);
@ -106,8 +106,6 @@ namespace BizHawk.Bizware.Graphics
RasterizerState = Device.CreateRasterizerState(rd);
Context.IASetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
foreach (var tex2d in Textures)
{
tex2d.CreateTexture();

View File

@ -2,11 +2,12 @@ using System;
using System.Drawing;
using System.Numerics;
using BizHawk.Common;
using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DXGI;
using BizHawk.Common;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
@ -216,6 +217,7 @@ namespace BizHawk.Bizware.Graphics
Context.IASetInputLayout(d3d11Pipeline.VertexInputLayout);
Context.IASetVertexBuffer(0, d3d11Pipeline.VertexBuffer, d3d11Pipeline.VertexStride);
Context.IASetIndexBuffer(d3d11Pipeline.IndexBuffer, Format.R16_UInt, 0);
// not sure if this applies to the current pipeline or all pipelines
// just set it every time to be safe
@ -262,63 +264,46 @@ namespace BizHawk.Bizware.Graphics
Context.OMSetRenderTargets(_controlSwapChain.RTV);
}
public void Draw(IntPtr data, int count)
private unsafe void UpdateConstantBuffers()
{
var pipeline = CurPipeline;
var stride = pipeline.VertexStride;
if (pipeline.VertexBufferCount < count)
for (var i = 0; i < ID3D11DeviceContext.CommonShaderConstantBufferSlotCount; i++)
{
pipeline.VertexBuffer?.Dispose();
var bd = new BufferDescription(stride * count, BindFlags.VertexBuffer, ResourceUsage.Dynamic, CpuAccessFlags.Write);
pipeline.VertexBuffer = Device.CreateBuffer(in bd, data);
pipeline.VertexBufferCount = count;
}
else
{
var mappedVb = Context.Map(pipeline.VertexBuffer, MapMode.WriteDiscard);
try
var pb = CurPipeline.PendingBuffers[i];
if (pb == null)
{
unsafe
{
Buffer.MemoryCopy((void*)data, (void*)mappedVb.DataPointer, stride * pipeline.VertexBufferCount, stride * count);
}
break;
}
finally
if (pb.VSBufferDirty)
{
Context.Unmap(pipeline.VertexBuffer);
var vsCb = Context.Map(CurPipeline.VSConstantBuffers[i], MapMode.WriteDiscard);
Buffer.MemoryCopy((void*)pb.VSPendingBuffer, (void*)vsCb.DataPointer, pb.VSBufferSize, pb.VSBufferSize);
Context.Unmap(CurPipeline.VSConstantBuffers[i]);
pb.VSBufferDirty = false;
}
if (pb.PSBufferDirty)
{
var psCb = Context.Map(CurPipeline.PSConstantBuffers[i], MapMode.WriteDiscard);
Buffer.MemoryCopy((void*)pb.PSPendingBuffer, (void*)psCb.DataPointer, pb.PSBufferSize, pb.PSBufferSize);
Context.Unmap(CurPipeline.PSConstantBuffers[i]);
pb.PSBufferDirty = false;
}
}
}
unsafe
{
for (var i = 0; i < ID3D11DeviceContext.CommonShaderConstantBufferSlotCount; i++)
{
var pb = pipeline.PendingBuffers[i];
if (pb == null)
{
break;
}
public void Draw(int vertexCount)
{
UpdateConstantBuffers();
Context.IASetPrimitiveTopology(PrimitiveTopology.TriangleStrip);
Context.Draw(vertexCount, 0);
}
if (pb.VSBufferDirty)
{
var vsCb = Context.Map(pipeline.VSConstantBuffers[i], MapMode.WriteDiscard);
Buffer.MemoryCopy((void*)pb.VSPendingBuffer, (void*)vsCb.DataPointer, pb.VSBufferSize, pb.VSBufferSize);
Context.Unmap(pipeline.VSConstantBuffers[i]);
pb.VSBufferDirty = false;
}
if (pb.PSBufferDirty)
{
var psCb = Context.Map(pipeline.PSConstantBuffers[i], MapMode.WriteDiscard);
Buffer.MemoryCopy((void*)pb.PSPendingBuffer, (void*)psCb.DataPointer, pb.PSBufferSize, pb.PSBufferSize);
Context.Unmap(pipeline.PSConstantBuffers[i]);
pb.PSBufferDirty = false;
}
}
}
Context.Draw(count, 0);
public void DrawIndexed(int indexCount, int indexStart, int vertexStart)
{
UpdateConstantBuffers();
Context.IASetPrimitiveTopology(PrimitiveTopology.TriangleList);
Context.DrawIndexed(indexCount, indexStart, vertexStart);
}
}
}

View File

@ -1,4 +1,5 @@
using System.Drawing;
using System.Numerics;
namespace BizHawk.Bizware.Graphics
{
@ -9,5 +10,8 @@ namespace BizHawk.Bizware.Graphics
width = size.Width;
height = size.Height;
}
public static Vector2 ToVector(this Point point)
=> new(point.X, point.Y);
}
}

View File

@ -11,6 +11,11 @@ namespace BizHawk.Bizware.Graphics
? new GDIPlusGuiRenderer(gdipImpl)
: new GuiRenderer(gl);
public static I2DRenderer Create2DRenderer(this IGL gl, ImGuiResourceCache resourceCache)
=> gl is IGL_GDIPlus gdipImpl
? new SDLImGui2DRenderer(gdipImpl, resourceCache)
: new ImGui2DRenderer(gl, resourceCache);
/// <summary>
/// Loads a texture from disk
/// </summary>

View File

@ -46,7 +46,11 @@ namespace BizHawk.Bizware.Graphics
{
}
public void Draw(IntPtr data, int count)
public void Draw(int vertexCount)
{
}
public void DrawIndexed(int indexCount, int indexStart, int vertexStart)
{
}

View File

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Drawing;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Shared resource cache for the ImGui 2D renderer
/// This allows multiple ImGui renderers to share the same cache
/// </summary>
public sealed class ImGuiResourceCache
{
private readonly IGL _igl;
private ITexture2D _lastTexture;
internal readonly IPipeline Pipeline;
internal readonly Dictionary<Bitmap, ITexture2D> TextureCache = new();
internal readonly Dictionary<Color, SolidBrush> BrushCache = new();
public ImGuiResourceCache(IGL igl)
{
_igl = igl;
if (igl.DispMethodEnum is EDispMethod.OpenGL or EDispMethod.D3D11)
{
string psProgram, vsProgram;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (igl.DispMethodEnum)
{
case EDispMethod.D3D11:
vsProgram = ImGuiVertexShader_d3d11;
psProgram = ImGuiPixelShader_d3d11;
break;
case EDispMethod.OpenGL:
vsProgram = ImGuiVertexShader_gl;
psProgram = ImGuiPixelShader_gl;
break;
default:
throw new InvalidOperationException();
}
var vertexLayoutItems = new PipelineCompileArgs.VertexLayoutItem[3];
vertexLayoutItems[0] = new("aPosition", 2, 0, AttribUsage.Position);
vertexLayoutItems[1] = new("aTexcoord", 2, 8, AttribUsage.Texcoord0);
vertexLayoutItems[2] = new("aColor", 4, 16, AttribUsage.Color0, Integer: true);
var compileArgs = new PipelineCompileArgs(
vertexLayoutItems,
vertexShaderArgs: new(vsProgram, "vsmain"),
fragmentShaderArgs: new(psProgram, "psmain"),
fragmentOutputName: "oColor");
Pipeline = igl.CreatePipeline(compileArgs);
igl.BindPipeline(Pipeline);
Pipeline.SetUniform("uSamplerEnable", false);
Pipeline.SetUniformSampler("uSampler0", null);
igl.BindPipeline(null);
}
}
internal void SetProjection(int width, int height)
{
var projection = _igl.CreateGuiViewMatrix(width, height) * _igl.CreateGuiProjectionMatrix(width, height);
Pipeline.SetUniformMatrix("um44Projection", projection);
}
internal void SetTexture(ITexture2D texture2D)
{
if (_lastTexture != texture2D)
{
Pipeline.SetUniform("uSamplerEnable", texture2D != null);
Pipeline.SetUniformSampler("uSampler0", texture2D);
_lastTexture = texture2D;
}
}
public void Dispose()
{
foreach (var cachedTexture in TextureCache.Values)
{
cachedTexture.Dispose();
}
foreach (var cachedBrush in BrushCache.Values)
{
cachedBrush.Dispose();
}
TextureCache.Clear();
BrushCache.Clear();
Pipeline?.Dispose();
}
public const string ImGuiVertexShader_d3d11 = @"
//vertex shader uniforms
float4x4 um44Projection;
struct VS_INPUT
{
float2 aPosition : POSITION;
float2 aTexcoord : TEXCOORD0;
float4 aColor : COLOR0;
};
struct VS_OUTPUT
{
float4 vPosition : SV_POSITION;
float2 vTexcoord0 : TEXCOORD0;
float4 vColor0 : COLOR0;
};
VS_OUTPUT vsmain(VS_INPUT src)
{
VS_OUTPUT dst;
float4 temp = float4(src.aPosition,0,1);
dst.vPosition = mul(um44Projection, temp);
dst.vTexcoord0 = src.aTexcoord;
dst.vColor0 = src.aColor;
return dst;
}
";
public const string ImGuiPixelShader_d3d11 = @"
//pixel shader uniforms
bool uSamplerEnable;
Texture2D uTexture0;
sampler uSampler0;
struct PS_INPUT
{
float4 vPosition : SV_POSITION;
float2 vTexcoord0 : TEXCOORD0;
float4 vColor0 : COLOR0;
};
float4 psmain(PS_INPUT src) : SV_Target
{
float4 temp = src.vColor0;
if(uSamplerEnable) temp *= uTexture0.Sample(uSampler0,src.vTexcoord0);
return temp;
}
";
public const string ImGuiVertexShader_gl = @"
//opengl 3.0
#version 130
uniform mat4 um44Projection;
in vec2 aPosition;
in vec2 aTexcoord;
in vec4 aColor;
out vec2 vTexcoord0;
out vec4 vColor0;
void main()
{
vec4 temp = vec4(aPosition,0,1);
gl_Position = um44Projection * temp;
vTexcoord0 = aTexcoord;
vColor0 = aColor;
}";
public const string ImGuiPixelShader_gl = @"
//opengl 3.0
#version 130
bool uSamplerEnable;
uniform sampler2D uSampler0;
in vec2 vTexcoord0;
in vec4 vColor0;
out vec4 oColor;
void main()
{
vec4 temp = vColor0;
if(uSamplerEnable) temp *= texture2D(uSampler0, vTexcoord0);
oColor = temp;
}";
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Interface for 2D rendering, similar to System.Drawing's Graphics
/// Semantically, this must be able to be called at any point
/// This means batching MUST occur
/// As in this case IGL resources can only be used in the ctor, Dispose(), and Render()
/// </summary>
public interface I2DRenderer : IDisposable
{
/// <summary>
/// Renders any pending draw calls.
/// Returns the result as an ITexture2D.
/// Internally, this may change the bound render target, pipeline, viewport/scissor, and blending state.
/// If this is important, rebind them after calling this.
/// Any rendering occurs after clearing the target texture.
/// If nothing needs to be rendered and the size does not change, the contents are preserved
/// </summary>
ITexture2D Render(int width, int height);
/// <summary>
/// Clears any pending draw calls.
/// This will also insert a command to clear the target texture.
/// </summary>
void Clear();
CompositingMode CompositingMode { set; }
void DrawBezier(Color color, Point pt1, Point pt2, Point pt3, Point pt4);
void DrawBeziers(Color color, Point[] points);
void DrawRectangle(Color color, int x, int y, int width, int height);
void FillRectangle(Color color, int x, int y, int width, int height);
void DrawEllipse(Color color, int x, int y, int width, int height);
void FillEllipse(Color color, int x, int y, int width, int height);
void DrawImage(Bitmap bitmap, int x, int y);
void DrawImage(Bitmap bitmap, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, bool cache);
void DrawLine(Color color, int x1, int y1, int x2, int y2);
void DrawPie(Color color, int x, int y, int width, int height, int startAngle, int sweepAngle);
void FillPie(Color color, int x, int y, int width, int height, int startAngle, int sweepAngle);
void DrawPolygon(Color color, Point[] points);
void FillPolygon(Color color, Point[] points);
void DrawString(string s, Font font, Color color, float x, float y, StringFormat format = null, TextRenderingHint textRenderingHint = TextRenderingHint.SystemDefault);
}
}

View File

@ -70,12 +70,17 @@ namespace BizHawk.Bizware.Graphics
void SetViewport(int x, int y, int width, int height);
/// <summary>
/// Draws based on the currently set pipeline
/// Data contains vertexes based on the pipeline's VertexLayout
/// Count is the vertex count
/// Non-indexed drawing for the currently set pipeline/render target
/// Vertexes must form triangle strips
/// </summary>
void Draw(IntPtr data, int count);
void Draw(int vertexCount);
/// <summary>
/// Indexed drawing for the currently set pipeline/render target
/// Indexes must be 16 bits each
/// Vertexes must form triangle lists
/// </summary>
void DrawIndexed(int indexCount, int indexStart, int vertexStart);
/// <summary>
/// Generates a proper 2D othographic projection for the given destination size, suitable for use in a GUI

View File

@ -5,6 +5,16 @@ namespace BizHawk.Bizware.Graphics
{
public interface IPipeline : IDisposable
{
/// <summary>
/// Sets vertex data for this pipeline
/// </summary>
void SetVertexData(IntPtr data, int count);
/// <summary>
/// Sets index data for this pipeline
/// </summary>
void SetIndexData(IntPtr data, int count);
bool HasUniformSampler(string name);
string GetUniformSamplerName(int index);

View File

@ -19,8 +19,8 @@ namespace BizHawk.Bizware.Graphics
private OpenGLPipeline _curPipeline;
internal bool DefaultRenderTargetBound;
// this IGL either requires at least OpenGL 3.0
public static bool Available => OpenGLVersion.SupportsVersion(3, 0);
// this IGL either requires at least OpenGL 3.2
public static bool Available => OpenGLVersion.SupportsVersion(3, 2);
public IGL_OpenGL()
{
@ -45,7 +45,7 @@ namespace BizHawk.Bizware.Graphics
{
GL.Enable(EnableCap.Blend);
GL.BlendEquation(GLEnum.FuncAdd);
GL.BlendFuncSeparate(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha, BlendingFactor.One, BlendingFactor.Zero);
GL.BlendFuncSeparate(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha, BlendingFactor.One, BlendingFactor.OneMinusSrcAlpha);
}
public void DisableBlending()
@ -71,6 +71,7 @@ namespace BizHawk.Bizware.Graphics
{
GL.BindVertexArray(0);
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
GL.UseProgram(0);
_curPipeline = null;
return;
@ -78,34 +79,15 @@ namespace BizHawk.Bizware.Graphics
GL.BindVertexArray(_curPipeline.VAO);
GL.BindBuffer(GLEnum.ArrayBuffer, _curPipeline.VBO);
GL.BindBuffer(GLEnum.ElementArrayBuffer, _curPipeline.IBO);
GL.UseProgram(_curPipeline.PID);
}
public void Draw(IntPtr data, int count)
{
if (_curPipeline == null)
{
throw new InvalidOperationException($"Tried to {nameof(Draw)} without pipeline!");
}
public void Draw(int vertexCount)
=> GL.DrawArrays(PrimitiveType.TriangleStrip, 0, (uint)vertexCount);
unsafe
{
var vertexes = new ReadOnlySpan<byte>((void*)data, count * _curPipeline.VertexStride);
// BufferData reallocs and BufferSubData doesn't, so only use the former if we need to grow the buffer
if (vertexes.Length > _curPipeline.VertexBufferLen)
{
GL.BufferData(GLEnum.ArrayBuffer, vertexes, GLEnum.DynamicDraw);
_curPipeline.VertexBufferLen = vertexes.Length;
}
else
{
GL.BufferSubData(GLEnum.ArrayBuffer, 0, vertexes);
}
}
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, (uint)count);
}
public unsafe void DrawIndexed(int indexCount, int indexStart, int vertexStart)
=> GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (uint)indexCount, DrawElementsType.UnsignedShort, (void*)indexStart, vertexStart);
public ITexture2D CreateTexture(int width, int height)
=> new OpenGLTexture2D(GL, width, height);

View File

@ -19,6 +19,9 @@ namespace BizHawk.Bizware.Graphics
public readonly int VertexStride;
public int VertexBufferLen;
public readonly uint IBO;
public int IndexBufferLen;
public readonly uint VertexSID;
public readonly uint FragmentSID;
@ -81,6 +84,8 @@ namespace BizHawk.Bizware.Graphics
VBO = GL.GenBuffer();
VertexStride = compileArgs.VertexLayoutStride;
IBO = GL.GenBuffer();
_ = GL.GetError();
VertexSID = CompileShader(compileArgs.VertexShaderArgs.Source, ShaderType.VertexShader);
FragmentSID = CompileShader(compileArgs.FragmentShaderArgs.Source, ShaderType.FragmentShader);
@ -101,9 +106,9 @@ namespace BizHawk.Bizware.Graphics
GL.EnableVertexAttribArray(attribIndex);
GL.VertexAttribPointer(
attribIndex,
item.Components,
VertexAttribPointerType.Float,
normalized: false,
item.Integer ? (int)GLEnum.Bgra : item.Components,
item.Integer ? VertexAttribPointerType.UnsignedByte : VertexAttribPointerType.Float,
normalized: item.Integer,
(uint)VertexStride,
(void*)item.Offset);
}
@ -205,6 +210,37 @@ namespace BizHawk.Bizware.Graphics
GL.DeleteBuffer(VBO);
}
public unsafe void SetVertexData(IntPtr data, int count)
{
var vertexes = new ReadOnlySpan<byte>((void*)data, count * VertexStride);
// BufferData reallocs and BufferSubData doesn't, so only use the former if we need to grow the buffer
if (vertexes.Length > VertexBufferLen)
{
GL.BufferData(GLEnum.ArrayBuffer, vertexes, GLEnum.DynamicDraw);
VertexBufferLen = vertexes.Length;
}
else
{
GL.BufferSubData(GLEnum.ArrayBuffer, 0, vertexes);
}
}
public unsafe void SetIndexData(IntPtr data, int count)
{
var indexes = new ReadOnlySpan<byte>((void*)data, count * 2);
if (indexes.Length > IndexBufferLen)
{
GL.BufferData(GLEnum.ElementArrayBuffer, indexes, GLEnum.DynamicDraw);
IndexBufferLen = indexes.Length;
}
else
{
GL.BufferSubData(GLEnum.ElementArrayBuffer, 0, indexes);
}
}
public bool HasUniformSampler(string name)
=> _samplers.ContainsKey(name);
@ -216,6 +252,11 @@ namespace BizHawk.Bizware.Graphics
public void SetUniformSampler(string name, ITexture2D tex)
{
if (tex == null)
{
return;
}
if (_samplers.TryGetValue(name, out var sampler))
{
var oglTex = (OpenGLTexture2D)tex;

View File

@ -10,12 +10,15 @@ namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Defines an item within a vertex layout
/// This is currently restricted to float types
/// This is currently restricted to to either 4 byte floats, or a 32 bit unsigned normalized integer type
/// If this is a 32 bit unsigned normalized type, it must have 4 components
/// </summary>
/// <param name="Components">Number of components in the item (e.g. float2 has 2 components). Only 1-4 components is valid</param>
/// <param name="Name">Name of the item</param>
/// <param name="Components">Number of components in the item (e.g. float2 has 2 components). Only 1-4 components is valid (unless Integer is true, then only 4 is valid)</param>
/// <param name="Offset">Byte offset within the vertex buffer to the item</param>
/// <param name="Usage">Semantic usage</param>
public readonly record struct VertexLayoutItem(string Name, int Components, int Offset, AttribUsage Usage);
/// <param name="Integer">Indicates if this uses an integer rather than a float</param>
public readonly record struct VertexLayoutItem(string Name, int Components, int Offset, AttribUsage Usage, bool Integer = false);
/// <summary>
/// Defines arguments for compiling a shader
@ -44,7 +47,12 @@ namespace BizHawk.Bizware.Graphics
throw new InvalidOperationException("A vertex layout item must have 1-4 components");
}
VertexLayoutStride += item.Components * 4;
if (item.Integer && item.Components != 4)
{
throw new InvalidOperationException("A vertex layout integer item must have 4 components");
}
VertexLayoutStride += item.Integer ? 4 : item.Components * 4;
}
VertexShaderArgs = vertexShaderArgs;

View File

@ -258,7 +258,8 @@ namespace BizHawk.Bizware.Graphics
pData[30] = CornerColors[3].Z;
pData[31] = CornerColors[3].W;
Owner.Draw(new(pData), 4);
CurrPipeline.SetVertexData(new(pData), 4);
Owner.Draw(4);
}
public bool IsActive { get; private set; }

View File

@ -0,0 +1,490 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Numerics;
using System.Runtime.InteropServices;
using ImGuiNET;
using BizHawk.Common.CollectionExtensions;
using SDGraphics = System.Drawing.Graphics;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Wraps ImGui to create a simple 2D renderer
/// </summary>
internal class ImGui2DRenderer : I2DRenderer
{
// ImDrawListSharedData is defined in imgui_internal.h, and therefore it is not exposed in ImGuiNET
// we want to use it directly however, and cimgui does give exports for it
[StructLayout(LayoutKind.Sequential)]
private struct ImDrawListSharedData
{
public Vector2 TexUvWhitePixel;
public IntPtr Font;
public float FontSize;
public float CurveTessellationTol;
// other fields are present, but we don't care about them
}
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
private static extern unsafe ImDrawListSharedData* ImDrawListSharedData_ImDrawListSharedData();
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
private static extern unsafe void ImDrawListSharedData_SetCircleTessellationMaxError(ImDrawListSharedData* self, float max_error);
private static readonly unsafe ImDrawListSharedData* _drawListSharedData;
static ImGui2DRenderer()
{
unsafe
{
_drawListSharedData = ImDrawListSharedData_ImDrawListSharedData();
// values taken from default ImGuiStyle
_drawListSharedData->CurveTessellationTol = 1.25f;
ImDrawListSharedData_SetCircleTessellationMaxError(_drawListSharedData, 0.3f);
}
}
private readonly HashSet<GCHandle> _gcHandles = new();
protected virtual float RenderThickness => 1;
protected readonly IGL _igl;
protected readonly ImGuiResourceCache _resourceCache;
protected ImDrawListPtr _imGuiDrawList;
protected bool _hasDrawStringCommand;
protected bool _hasClearPending;
protected Bitmap _stringOutput;
protected SDGraphics _stringGraphics;
protected IRenderTarget _renderTarget;
public ImGui2DRenderer(IGL igl, ImGuiResourceCache resourceCache)
{
_igl = igl;
_resourceCache = resourceCache;
unsafe
{
_imGuiDrawList = ImGuiNative.ImDrawList_ImDrawList((IntPtr)_drawListSharedData);
}
_pendingBlendEnable = true;
ResetDrawList();
}
public void Dispose()
{
ClearGCHandles();
_renderTarget?.Dispose();
_renderTarget = null;
_stringGraphics?.Dispose();
_stringGraphics = null;
_stringOutput?.Dispose();
_stringOutput = null;
_imGuiDrawList.Destroy();
_imGuiDrawList = IntPtr.Zero;
}
private unsafe void ClearStringOutput()
{
var bmpData = _stringOutput.LockBits(new(0, 0, _stringOutput.Width, _stringOutput.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
new Span<byte>((void*)bmpData.Scan0, bmpData.Stride * bmpData.Height).Clear();
_stringOutput.UnlockBits(bmpData);
}
private void ClearGCHandles()
{
foreach (var gcHandle in _gcHandles)
{
switch (gcHandle.Target)
{
case ImGuiUserTexture userTexture:
// avoid disposing our string output bitmap here
if (userTexture.Bitmap != _stringOutput)
{
userTexture.Bitmap.Dispose();
}
break;
case DrawStringArgs args:
args.Font.Dispose();
break;
}
gcHandle.Free();
}
_gcHandles.Clear();
}
private void ResetDrawList()
{
ClearGCHandles();
_imGuiDrawList._ResetForNewFrame();
_imGuiDrawList.Flags |= ImDrawListFlags.AllowVtxOffset;
_hasDrawStringCommand = false;
EnableBlending = _pendingBlendEnable;
}
protected class ImGuiUserTexture
{
public Bitmap Bitmap;
public ITexture2D CachedTexture;
public bool WantCache;
}
protected class DrawStringArgs
{
public string Str;
public Font Font;
public Color Color;
public float X, Y;
public StringFormat Format;
public TextRenderingHint TextRenderingHint;
}
protected enum DrawCallbackId
{
None,
DisableBlending,
EnableBlending,
DrawString,
}
protected virtual void RenderInternal(int width, int height)
{
if (EnableBlending)
{
_igl.EnableBlending();
}
else
{
_igl.DisableBlending();
}
_igl.BindPipeline(_resourceCache.Pipeline);
_resourceCache.SetProjection(width, height);
if (_imGuiDrawList.VtxBuffer.Size > 0)
{
_resourceCache.Pipeline.SetVertexData(_imGuiDrawList.VtxBuffer.Data, _imGuiDrawList.VtxBuffer.Size);
}
if (_imGuiDrawList.IdxBuffer.Size > 0)
{
_resourceCache.Pipeline.SetIndexData(_imGuiDrawList.IdxBuffer.Data, _imGuiDrawList.IdxBuffer.Size);
}
var cmdBuffer = _imGuiDrawList.CmdBuffer;
for (var i = 0; i < cmdBuffer.Size; i++)
{
var cmd = cmdBuffer[i];
var callbackId = (DrawCallbackId)cmd.UserCallback;
switch (callbackId)
{
case DrawCallbackId.None:
{
var texId = cmd.GetTexID();
ITexture2D tempTex = null;
if (texId != IntPtr.Zero)
{
var userTex = (ImGuiUserTexture)GCHandle.FromIntPtr(texId).Target!;
if (userTex.CachedTexture != null)
{
_resourceCache.SetTexture(userTex.CachedTexture);
}
else
{
tempTex = _igl.LoadTexture(userTex.Bitmap);
_resourceCache.SetTexture(tempTex);
if (userTex.WantCache)
{
_resourceCache.TextureCache.Add(userTex.Bitmap, tempTex);
userTex.CachedTexture = tempTex;
tempTex = null;
}
}
}
else
{
_resourceCache.SetTexture(null);
}
_igl.DrawIndexed((int)cmd.ElemCount, (int)cmd.IdxOffset, (int)cmd.VtxOffset);
tempTex?.Dispose();
break;
}
case DrawCallbackId.DisableBlending:
_igl.DisableBlending();
break;
case DrawCallbackId.EnableBlending:
_igl.EnableBlending();
break;
case DrawCallbackId.DrawString:
{
var stringArgs = (DrawStringArgs)GCHandle.FromIntPtr(cmd.UserCallbackData).Target!;
var brush = _resourceCache.BrushCache.GetValueOrPutNew1(stringArgs.Color);
_stringGraphics.TextRenderingHint = stringArgs.TextRenderingHint;
_stringGraphics.DrawString(stringArgs.Str, stringArgs.Font, brush, stringArgs.X, stringArgs.Y, stringArgs.Format);
break;
}
default:
throw new InvalidOperationException();
}
}
}
public ITexture2D Render(int width, int height)
{
var needsRender = _imGuiDrawList.VtxBuffer.Size > 0 || _imGuiDrawList.IdxBuffer.Size > 0 || _hasDrawStringCommand;
var needsClear = needsRender || _hasClearPending;
if (_renderTarget == null || _renderTarget.Width != width || _renderTarget.Height != height)
{
_renderTarget?.Dispose();
_renderTarget = _igl.CreateRenderTarget(width, height);
needsClear = true;
}
if (_hasDrawStringCommand
&& (_stringOutput == null
|| _stringOutput.Width != width
|| _stringOutput.Height != height))
{
_stringGraphics?.Dispose();
_stringOutput?.Dispose();
_stringOutput = new(width, height, PixelFormat.Format32bppArgb);
_stringGraphics = SDGraphics.FromImage(_stringOutput);
}
_renderTarget.Bind();
_igl.SetViewport(width, height);
if (needsClear)
{
_igl.ClearColor(Color.FromArgb(0));
_hasClearPending = false;
}
if (needsRender)
{
if (_hasDrawStringCommand)
{
ClearStringOutput();
// synthesize an add image command for our string bitmap
_imGuiDrawList.AddCallback((IntPtr)DrawCallbackId.EnableBlending, IntPtr.Zero);
DrawImage(_stringOutput, 0, 0);
}
RenderInternal(width, height);
ResetDrawList();
}
return _renderTarget;
}
public void Clear()
{
ResetDrawList();
_hasClearPending = true;
}
protected bool EnableBlending { get; private set; }
private bool _pendingBlendEnable;
public CompositingMode CompositingMode
{
set
{
switch (_pendingBlendEnable)
{
// CompositingMode.SourceCopy means disable blending
case true when value == CompositingMode.SourceCopy:
_imGuiDrawList.AddCallback((IntPtr)DrawCallbackId.DisableBlending, IntPtr.Zero);
_pendingBlendEnable = false;
break;
// CompositingMode.SourceOver means enable blending
case false when value == CompositingMode.SourceOver:
_imGuiDrawList.AddCallback((IntPtr)DrawCallbackId.EnableBlending, IntPtr.Zero);
_pendingBlendEnable = true;
break;
}
}
}
public void DrawBezier(Color color, Point pt1, Point pt2, Point pt3, Point pt4)
{
_imGuiDrawList.AddBezierCubic(
p1: pt1.ToVector(),
p2: pt2.ToVector(),
p3: pt3.ToVector(),
p4: pt4.ToVector(),
col: (uint)color.ToArgb(),
thickness: RenderThickness);
}
public void DrawBeziers(Color color, Point[] points)
{
if (points.Length < 4 || (points.Length - 1) % 3 != 0)
{
throw new InvalidOperationException("Invalid number of points");
}
var startPt = points[0];
var col = (uint)color.ToArgb();
for (var i = 1; i < points.Length; i += 3)
{
_imGuiDrawList.AddBezierCubic(
p1: startPt.ToVector(),
p2: points[i + 0].ToVector(),
p3: points[i + 1].ToVector(),
p4: points[i + 2].ToVector(),
col: col,
thickness: RenderThickness);
startPt = points[i + 2];
}
}
public void DrawRectangle(Color color, int x, int y, int width, int height)
{
_imGuiDrawList.AddRect(
p_min: new(x, y),
p_max: new(x + width, y + height),
col: (uint)color.ToArgb(),
rounding: 0,
flags: ImDrawFlags.None,
thickness: RenderThickness);
}
public void FillRectangle(Color color, int x, int y, int width, int height)
{
_imGuiDrawList.AddRectFilled(
p_min: new(x, y),
p_max: new(x + width, y + height),
col: (uint)color.ToArgb());
}
public void DrawEllipse(Color color, int x, int y, int width, int height)
{
var radius = new Vector2(width / 2.0f, height / 2.0f);
_imGuiDrawList.AddEllipse(
center: new(x + radius.X, y + radius.Y),
radius: radius,
col: (uint)color.ToArgb(),
rot: 0,
num_segments: 0,
RenderThickness);
}
public void FillEllipse(Color color, int x, int y, int width, int height)
{
var radius = new Vector2(width / 2.0f, height / 2.0f);
_imGuiDrawList.AddEllipseFilled(
center: new(x + radius.X, y + radius.Y),
radius: radius,
col: (uint)color.ToArgb());
}
public void DrawImage(Bitmap image, int x, int y)
{
var texture = new ImGuiUserTexture { Bitmap = image, WantCache = false };
var handle = GCHandle.Alloc(texture, GCHandleType.Normal);
_gcHandles.Add(handle);
_imGuiDrawList.AddImage(
user_texture_id: GCHandle.ToIntPtr(handle),
p_min: new(x, y),
p_max: new(x + image.Width, y + image.Height));
}
public void DrawImage(Bitmap image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, bool cache)
{
var texture = new ImGuiUserTexture { Bitmap = image, WantCache = cache };
if (cache && _resourceCache.TextureCache.TryGetValue(image, out var cachedTexture))
{
texture.CachedTexture = cachedTexture;
}
var handle = GCHandle.Alloc(texture, GCHandleType.Normal);
_gcHandles.Add(handle);
var imgWidth = (float)image.Width;
var imgHeight = (float)image.Height;
_imGuiDrawList.AddImage(
user_texture_id: GCHandle.ToIntPtr(handle),
p_min: new(destRect.Left, destRect.Top),
p_max: new(destRect.Right, destRect.Bottom),
uv_min: new(srcX / imgWidth, srcY / imgHeight),
uv_max: new((srcX + srcWidth) / imgWidth, (srcY + srcHeight) / imgHeight));
}
public void DrawLine(Color color, int x1, int y1, int x2, int y2)
{
_imGuiDrawList.AddLine(
p1: new(x1, y1),
p2: new(x2, y2),
col: (uint)color.ToArgb(),
thickness: RenderThickness);
}
public void DrawPie(Color color, int x, int y, int width, int height, int startAngle, int sweepAngle)
{
var radius = new Vector2(width / 2.0f, height / 2.0f);
var center = new Vector2(x + radius.X, y + radius.Y);
var aMin = (float)(Math.PI / 180 * startAngle);
var aMax = (float)(Math.PI / 180 * (startAngle + sweepAngle));
_imGuiDrawList.PathEllipticalArcTo(center, radius, 0, aMin, aMax);
_imGuiDrawList.PathLineTo(center);
_imGuiDrawList.PathStroke((uint)color.ToArgb(), ImDrawFlags.Closed, RenderThickness);
}
public void FillPie(Color color, int x, int y, int width, int height, int startAngle, int sweepAngle)
{
var radius = new Vector2(width / 2.0f, height / 2.0f);
var center = new Vector2(x + radius.X, y + radius.Y);
var aMin = (float)(Math.PI / 180 * startAngle);
var aMax = (float)(Math.PI / 180 * (startAngle + sweepAngle));
_imGuiDrawList.PathEllipticalArcTo(center, radius, 0, aMin, aMax);
_imGuiDrawList.PathLineTo(center);
_imGuiDrawList.PathFillConvex((uint)color.ToArgb());
}
public unsafe void DrawPolygon(Color color, Point[] points)
{
var vectorPoints = Array.ConvertAll(points, static p => p.ToVector());
fixed (Vector2* p = vectorPoints)
{
_imGuiDrawList.AddPolyline(
points: ref *p,
num_points: vectorPoints.Length,
col: (uint)color.ToArgb(),
flags: ImDrawFlags.Closed,
thickness: RenderThickness);
}
}
public unsafe void FillPolygon(Color color, Point[] points)
{
var vectorPoints = Array.ConvertAll(points, static p => p.ToVector());
fixed (Vector2* p = vectorPoints)
{
_imGuiDrawList.AddConvexPolyFilled(
points: ref *p,
num_points: vectorPoints.Length,
col: (uint)color.ToArgb());
}
}
public void DrawString(string s, Font font, Color color, float x, float y, StringFormat format, TextRenderingHint textRenderingHint)
{
var stringArgs = new DrawStringArgs { Str = s, Font = font, Color = color, X = x, Y = y, Format = format, TextRenderingHint = textRenderingHint };
var handle = GCHandle.Alloc(stringArgs, GCHandleType.Normal);
_gcHandles.Add(handle);
_imGuiDrawList.AddCallback((IntPtr)DrawCallbackId.DrawString, GCHandle.ToIntPtr(handle));
_hasDrawStringCommand = true;
}
}
}

View File

@ -0,0 +1,190 @@
using System;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using ImGuiNET;
using BizHawk.Common.CollectionExtensions;
using static SDL2.SDL;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Wraps SDL2's software rendering with an ImGui 2D renderer
/// Used for the GDI+ IGL, which doesn't understand vertexes and such
/// </summary>
internal class SDLImGui2DRenderer : ImGui2DRenderer
{
// SDL's software renderer sometimes doesn't fill in shapes all the way with a thickness of 1
// a thickness of 2 seems to suffice however
protected override float RenderThickness => 2.0f;
public SDLImGui2DRenderer(IGL_GDIPlus gdiPlus, ImGuiResourceCache resourceCache)
: base(gdiPlus, resourceCache)
{
_ = gdiPlus; // this backend must use the GDI+ display method, as it assumes IRenderTarget is an GDIPlusRenderTarget
}
private readonly ref struct SDLSurface
{
public readonly IntPtr Surface;
public SDLSurface(BitmapData bmpData)
{
Surface = SDL_CreateRGBSurfaceWithFormatFrom(
bmpData.Scan0, bmpData.Width, bmpData.Height, 8, bmpData.Stride, SDL_PIXELFORMAT_ABGR8888);
if (Surface == IntPtr.Zero)
{
throw new($"Failed to create SDL surface, SDL error: {SDL_GetError()}");
}
}
public void Dispose()
=> SDL_FreeSurface(Surface);
}
private readonly ref struct SDLSoftwareRenderer
{
public readonly IntPtr Renderer;
public SDLSoftwareRenderer(SDLSurface surface)
{
Renderer = SDL_CreateSoftwareRenderer(surface.Surface);
if (Renderer == IntPtr.Zero)
{
throw new($"Failed to create SDL software renderer, SDL error: {SDL_GetError()}");
}
}
public void Dispose()
=> SDL_DestroyRenderer(Renderer);
}
// SDL2-CS import expects float[]/int[]'s instead of raw pointers :(
[DllImport("SDL2", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
private static extern int SDL_RenderGeometryRaw(
IntPtr renderer,
IntPtr texture,
IntPtr xy,
int xy_stride,
IntPtr color,
int color_stride,
IntPtr uv,
int uv_stride,
int num_vertices,
IntPtr indices,
int num_indices,
int size_indices
);
private static unsafe void RenderCommand(IntPtr sdlRenderer, IntPtr sdlTexture, ImDrawListPtr cmdList, ImDrawCmdPtr cmd)
{
var vtxBuffer = (ImDrawVert*)cmdList.VtxBuffer.Data;
var idxBuffer = (ushort*)cmdList.IdxBuffer.Data;
var vtx = &vtxBuffer![cmd.VtxOffset];
_ = SDL_RenderGeometryRaw(
renderer: sdlRenderer,
texture: sdlTexture,
xy: (IntPtr)(&vtx->pos),
xy_stride: sizeof(ImDrawVert),
color: (IntPtr)(&vtx->col),
color_stride: sizeof(ImDrawVert),
uv: (IntPtr)(&vtx->uv),
uv_stride: sizeof(ImDrawVert),
num_vertices: (int)(cmdList.VtxBuffer.Size - cmd.VtxOffset),
indices: (IntPtr)(&idxBuffer![cmd.IdxOffset]),
num_indices: (int)cmd.ElemCount,
size_indices: sizeof(ushort)
);
}
protected override void RenderInternal(int width, int height)
{
var rt = (GDIPlusRenderTarget)_renderTarget;
var bmpData = rt.SDBitmap.LockBits(rt.GetRectangle(), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
try
{
using var surface = new SDLSurface(bmpData);
using var renderer = new SDLSoftwareRenderer(surface);
var sdlRenderer = renderer.Renderer;
_ = SDL_SetRenderDrawBlendMode(sdlRenderer, EnableBlending
? SDL_BlendMode.SDL_BLENDMODE_BLEND
: SDL_BlendMode.SDL_BLENDMODE_NONE);
var rect = new SDL_Rect { x = 0, y = 0, w = width, h = height };
_ = SDL_RenderSetViewport(sdlRenderer, ref rect);
_ = SDL_RenderSetClipRect(sdlRenderer, IntPtr.Zero);
var cmdBuffer = _imGuiDrawList.CmdBuffer;
for (var i = 0; i < cmdBuffer.Size; i++)
{
var cmd = cmdBuffer[i];
var callbackId = (DrawCallbackId)cmd.UserCallback;
switch (callbackId)
{
case DrawCallbackId.None:
{
var texId = cmd.GetTexID();
if (texId != IntPtr.Zero)
{
var userTex = (ImGuiUserTexture)GCHandle.FromIntPtr(texId).Target!;
var texBmpData = userTex.Bitmap.LockBits(
new(0, 0, userTex.Bitmap.Width, userTex.Bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
using var texSurf = new SDLSurface(texBmpData);
var sdlTex = SDL_CreateTextureFromSurface(sdlRenderer, texSurf.Surface);
if (sdlTex == IntPtr.Zero)
{
throw new($"Failed to create SDL texture from surface, SDL error: {SDL_GetError()}");
}
try
{
RenderCommand(sdlRenderer, sdlTex, _imGuiDrawList, cmd);
}
finally
{
SDL_DestroyTexture(sdlTex);
}
}
finally
{
userTex.Bitmap.UnlockBits(texBmpData);
}
}
else
{
RenderCommand(sdlRenderer, IntPtr.Zero, _imGuiDrawList, cmd);
}
break;
}
case DrawCallbackId.DisableBlending:
_ = SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BlendMode.SDL_BLENDMODE_NONE);
break;
case DrawCallbackId.EnableBlending:
_ = SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BlendMode.SDL_BLENDMODE_BLEND);
break;
case DrawCallbackId.DrawString:
{
var stringArgs = (DrawStringArgs)GCHandle.FromIntPtr(cmd.UserCallbackData).Target!;
var brush = _resourceCache.BrushCache.GetValueOrPutNew1(stringArgs.Color);
_stringGraphics.TextRenderingHint = stringArgs.TextRenderingHint;
_stringGraphics.DrawString(stringArgs.Str, stringArgs.Font, brush, stringArgs.X, stringArgs.Y, stringArgs.Format);
break;
}
default:
throw new InvalidOperationException();
}
}
}
finally
{
rt.SDBitmap.UnlockBits(bmpData);
}
}
}
}

View File

@ -133,8 +133,10 @@ namespace BizHawk.Bizware.Graphics
pData[i++] = 0; pData[i++] = 0; pData[i++] = 0; pData[i++] = 0; //useless color
pData[i++] = 1; pData[i] = v1;
Pipeline.SetVertexData(new(pData), 4);
Owner.DisableBlending();
Owner.Draw(new(pData), 4);
Owner.Draw(4);
}
public IGL Owner { get; }

View File

@ -6,6 +6,7 @@ using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using BizHawk.Bizware.Graphics;
using BizHawk.Common.CollectionExtensions;
using BizHawk.Emulation.Common;
@ -25,15 +26,9 @@ namespace BizHawk.Client.Common
private readonly DisplayManagerBase _displayManager;
private readonly Dictionary<string, Image> _imageCache = new Dictionary<string, Image>();
private readonly Dictionary<string, Bitmap> _imageCache = new();
private readonly Bitmap _nullGraphicsBitmap = new Bitmap(1, 1);
private readonly Dictionary<Color, Pen> _pens = new Dictionary<Color, Pen>();
private readonly Dictionary<Color, SolidBrush> _solidBrushes = new Dictionary<Color, SolidBrush>();
private ImageAttributes _attributes = new ImageAttributes();
private readonly Bitmap _nullGraphicsBitmap = new(1, 1);
private CompositingMode _compositingMode = CompositingMode.SourceOver;
@ -43,19 +38,12 @@ namespace BizHawk.Client.Common
private int _defaultPixelFont = 1; // = "gens"
private Color? _defaultTextBackground = Color.FromArgb(128, 0, 0, 0);
private IDisplaySurface _clientSurface;
private IDisplaySurface _GUISurface;
private Color _defaultTextBackground = Color.FromArgb(128, 0, 0, 0);
private (int Left, int Top, int Right, int Bottom) _padding = (0, 0, 0, 0);
private DisplaySurfaceID? _usingSurfaceID = null;
public bool EnableLuaAutolockHack = false;
public bool HasGUISurface => _GUISurface != null;
private DisplaySurfaceID? _usingSurfaceID;
public bool HasGUISurface => true;
public GuiApi(Action<string> logCallback, DisplayManagerBase displayManager)
{
@ -63,90 +51,23 @@ namespace BizHawk.Client.Common
_displayManager = displayManager;
}
private SolidBrush GetBrush(Color color)
=> _solidBrushes.GetValueOrPutNew1(color);
private Pen GetPen(Color color)
=> _pens.GetValueOrPutNew1(color);
private Graphics GetGraphics(DisplaySurfaceID? surfaceID)
private I2DRenderer Get2DRenderer(DisplaySurfaceID? surfaceID)
{
var g = GetRelevantSurface(surfaceID)?.GetGraphics() ?? Graphics.FromImage(_nullGraphicsBitmap);
return g;
var nnID = surfaceID ?? _usingSurfaceID ?? throw new Exception();
return _displayManager.GetApiHawk2DRenderer(nnID);
}
public void ToggleCompositingMode()
=> _compositingMode = (CompositingMode) (1 - (int) _compositingMode); // enum has two members, 0 and 1
public ImageAttributes GetAttributes() => _attributes;
public ImageAttributes GetAttributes() => null;
public void SetAttributes(ImageAttributes a) => _attributes = a;
private IDisplaySurface GetRelevantSurface(DisplaySurfaceID? surfaceID)
public void SetAttributes(ImageAttributes a)
{
var nnID = surfaceID ?? _usingSurfaceID ?? throw new Exception();
void ThisIsTheLuaAutolockHack()
{
try
{
UnlockSurface(nnID);
LockSurface(nnID);
}
catch (InvalidOperationException ex)
{
LogCallback(ex.ToString());
}
}
switch (nnID)
{
case DisplaySurfaceID.EmuCore:
if (_GUISurface == null && EnableLuaAutolockHack) ThisIsTheLuaAutolockHack();
return _GUISurface;
case DisplaySurfaceID.Client:
if (_clientSurface == null && EnableLuaAutolockHack) ThisIsTheLuaAutolockHack();
return _clientSurface;
default:
throw new Exception();
}
}
private void LockSurface(DisplaySurfaceID surfaceID)
{
switch (surfaceID)
{
case DisplaySurfaceID.EmuCore:
if (_GUISurface != null) throw new InvalidOperationException("attempt to lock surface without unlocking previous");
_GUISurface = _displayManager.LockApiHawkSurface(surfaceID, clear: true);
break;
case DisplaySurfaceID.Client:
if (_clientSurface != null) throw new InvalidOperationException("attempt to lock surface without unlocking previous");
_clientSurface = _displayManager.LockApiHawkSurface(surfaceID, clear: true);
break;
default:
throw new ArgumentException(message: "not a valid enum member", paramName: nameof(surfaceID));
}
}
private void UnlockSurface(DisplaySurfaceID surfaceID)
{
switch (surfaceID)
{
case DisplaySurfaceID.EmuCore:
if (_GUISurface != null) _displayManager.UnlockApiHawkSurface(_GUISurface);
_GUISurface = null;
break;
case DisplaySurfaceID.Client:
if (_clientSurface != null) _displayManager.UnlockApiHawkSurface(_clientSurface);
_clientSurface = null;
break;
default:
throw new ArgumentException(message: "not a valid enum member", paramName: nameof(surfaceID));
}
}
public void WithSurface(DisplaySurfaceID surfaceID, Action drawingCallsFunc)
{
LockSurface(surfaceID);
_usingSurfaceID = surfaceID;
try
{
@ -155,27 +76,9 @@ namespace BizHawk.Client.Common
finally
{
_usingSurfaceID = null;
UnlockSurface(surfaceID);
}
}
public readonly ref struct LuaAutoUnlockHack
{
private readonly GuiApi _guiApi;
internal LuaAutoUnlockHack(GuiApi guiApi)
=> _guiApi = guiApi;
public void Dispose()
{
_guiApi.UnlockSurface(DisplaySurfaceID.EmuCore);
_guiApi.UnlockSurface(DisplaySurfaceID.Client);
}
}
public LuaAutoUnlockHack ThisIsTheLuaAutoUnlockHack()
=> new(this);
public void DrawNew(string name, bool clear)
{
switch (name)
@ -204,7 +107,7 @@ namespace BizHawk.Client.Common
public void AddMessage(string message, int? duration = null)
=> _displayManager.OSD.AddMessage(message, duration);
public void ClearGraphics(DisplaySurfaceID? surfaceID = null) => GetRelevantSurface(surfaceID).Clear();
public void ClearGraphics(DisplaySurfaceID? surfaceID = null) => Get2DRenderer(surfaceID).Clear();
public void ClearText() => _displayManager.OSD.ClearGuiText();
@ -212,7 +115,7 @@ namespace BizHawk.Client.Common
public void SetDefaultBackgroundColor(Color color) => _defaultBackground = color;
public Color? GetDefaultTextBackground() => _defaultTextBackground;
public Color GetDefaultTextBackground() => _defaultTextBackground;
public void SetDefaultTextBackground(Color color) => _defaultTextBackground = color;
@ -238,9 +141,9 @@ namespace BizHawk.Client.Common
{
try
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawBezier(GetPen(color ?? _defaultForeground), p1, p2, p3, p4);
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawBezier(color ?? _defaultForeground, p1, p2, p3, p4);
}
catch (Exception)
{
@ -252,9 +155,9 @@ namespace BizHawk.Client.Common
{
try
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawBeziers(GetPen(color ?? _defaultForeground), points);
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawBeziers(color ?? _defaultForeground, points);
}
catch (Exception)
{
@ -266,7 +169,7 @@ namespace BizHawk.Client.Common
{
try
{
float w;
int w;
if (x < x2)
{
w = x2 - x;
@ -275,9 +178,9 @@ namespace BizHawk.Client.Common
{
x2 = x - x2;
x -= x2;
w = Math.Max(x2, 0.1f);
w = Math.Max(x2, 1);
}
float h;
int h;
if (y < y2)
{
h = y2 - y;
@ -286,13 +189,13 @@ namespace BizHawk.Client.Common
{
y2 = y - y2;
y -= y2;
h = Math.Max(y2, 0.1f);
h = Math.Max(y2, 1);
}
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawRectangle(GetPen(line ?? _defaultForeground), x, y, w, h);
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawRectangle(line ?? _defaultForeground, x, y, w, h);
var bg = background ?? _defaultBackground;
if (bg != null) g.FillRectangle(GetBrush(bg.Value), x + 1, y + 1, Math.Max(w - 1, 0), Math.Max(h - 1, 0));
if (bg != null) r.FillRectangle(bg.Value, x + 1, y + 1, Math.Max(w - 2, 0), Math.Max(h - 2, 0));
}
catch (Exception)
{
@ -304,11 +207,11 @@ namespace BizHawk.Client.Common
{
try
{
using var g = GetGraphics(surfaceID);
var r = Get2DRenderer(surfaceID);
var bg = background ?? _defaultBackground;
if (bg != null) g.FillEllipse(GetBrush(bg.Value), x, y, width, height);
g.CompositingMode = _compositingMode;
g.DrawEllipse(GetPen(line ?? _defaultForeground), x, y, width, height);
if (bg != null) r.FillEllipse(bg.Value, x, y, width, height);
r.CompositingMode = _compositingMode;
r.DrawEllipse(line ?? _defaultForeground, x, y, width, height);
}
catch (Exception)
{
@ -325,15 +228,14 @@ namespace BizHawk.Client.Common
AddMessage($"File not found: {path}");
return;
}
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawIcon(
width != null && height != null
? new Icon(path, width.Value, height.Value)
: new Icon(path),
x,
y
);
var icon = width != null && height != null
? new Icon(path, width.Value, height.Value)
: new Icon(path);
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawImage(icon.ToBitmap(), x, y);
}
catch (Exception)
{
@ -343,19 +245,19 @@ namespace BizHawk.Client.Common
public void DrawImage(Image img, int x, int y, int? width = null, int? height = null, bool cache = true, DisplaySurfaceID? surfaceID = null)
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawImage(
img,
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawImage(
new(img),
new Rectangle(x, y, width ?? img.Width, height ?? img.Height),
0,
0,
img.Width,
img.Height,
GraphicsUnit.Pixel,
_attributes
cache: false // caching is meaningless here
);
}
public void DrawImage(string path, int x, int y, int? width = null, int? height = null, bool cache = true, DisplaySurfaceID? surfaceID = null)
{
if (!File.Exists(path))
@ -363,44 +265,41 @@ namespace BizHawk.Client.Common
LogCallback($"File not found: {path}");
return;
}
using var g = GetGraphics(surfaceID);
if (!_imageCache.TryGetValue(path, out var img))
{
img = Image.FromFile(path);
img = new(Image.FromFile(path));
if (cache) _imageCache[path] = img;
}
g.CompositingMode = _compositingMode;
g.DrawImage(
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawImage(
img,
new Rectangle(x, y, width ?? img.Width, height ?? img.Height),
0,
0,
img.Width,
img.Height,
GraphicsUnit.Pixel,
_attributes
cache
);
}
public void ClearImageCache()
{
foreach (var image in _imageCache) image.Value.Dispose();
_imageCache.Clear();
}
=> _imageCache.Clear();
public void DrawImageRegion(Image img, int source_x, int source_y, int source_width, int source_height, int dest_x, int dest_y, int? dest_width = null, int? dest_height = null, DisplaySurfaceID? surfaceID = null)
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawImage(
img,
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawImage(
new(img),
new Rectangle(dest_x, dest_y, dest_width ?? source_width, dest_height ?? source_height),
source_x,
source_y,
source_width,
source_height,
GraphicsUnit.Pixel,
_attributes
cache: false
);
}
@ -411,25 +310,25 @@ namespace BizHawk.Client.Common
LogCallback($"File not found: {path}");
return;
}
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawImage(
_imageCache.GetValueOrPut(path, Image.FromFile),
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawImage(
_imageCache.GetValueOrPut(path, static i => new(Image.FromFile(i))),
new Rectangle(dest_x, dest_y, dest_width ?? source_width, dest_height ?? source_height),
source_x,
source_y,
source_width,
source_height,
GraphicsUnit.Pixel,
_attributes
cache: true
);
}
public void DrawLine(int x1, int y1, int x2, int y2, Color? color = null, DisplaySurfaceID? surfaceID = null)
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
g.DrawLine(GetPen(color ?? _defaultForeground), x1, y1, x2, y2);
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
r.DrawLine(color ?? _defaultForeground, x1, y1, x2, y2);
}
public void DrawAxis(int x, int y, int size, Color? color = null, DisplaySurfaceID? surfaceID = null)
@ -440,19 +339,19 @@ namespace BizHawk.Client.Common
public void DrawPie(int x, int y, int width, int height, int startangle, int sweepangle, Color? line = null, Color? background = null, DisplaySurfaceID? surfaceID = null)
{
using var g = GetGraphics(surfaceID);
g.CompositingMode = _compositingMode;
var r = Get2DRenderer(surfaceID);
r.CompositingMode = _compositingMode;
var bg = background ?? _defaultBackground;
if (bg != null) g.FillPie(GetBrush(bg.Value), x, y, width, height, startangle, sweepangle);
g.DrawPie(GetPen(line ?? _defaultForeground), x + 1, y + 1, width - 1, height - 1, startangle, sweepangle);
if (bg != null) r.FillPie(bg.Value, x, y, width, height, startangle, sweepangle);
r.DrawPie(line ?? _defaultForeground, x + 1, y + 1, width - 1, height - 1, startangle, sweepangle);
}
public void DrawPixel(int x, int y, Color? color = null, DisplaySurfaceID? surfaceID = null)
{
try
{
using var g = GetGraphics(surfaceID);
g.DrawLine(GetPen(color ?? _defaultForeground), x, y, x + 0.1F, y);
var r = Get2DRenderer(surfaceID);
r.DrawLine(color ?? _defaultForeground, x, y, x + 1, y);
}
catch (Exception)
{
@ -464,10 +363,10 @@ namespace BizHawk.Client.Common
{
try
{
using var g = GetGraphics(surfaceID);
g.DrawPolygon(GetPen(line ?? _defaultForeground), points);
var r = Get2DRenderer(surfaceID);
r.DrawPolygon(line ?? _defaultForeground, points);
var bg = background ?? _defaultBackground;
if (bg != null) g.FillPolygon(GetBrush(bg.Value), points);
if (bg != null) r.FillPolygon(bg.Value, points);
}
catch (Exception)
{
@ -477,12 +376,12 @@ namespace BizHawk.Client.Common
public void DrawRectangle(int x, int y, int width, int height, Color? line = null, Color? background = null, DisplaySurfaceID? surfaceID = null)
{
using var g = GetGraphics(surfaceID);
var w = Math.Max(width, 0.1F);
var h = Math.Max(height, 0.1F);
g.DrawRectangle(GetPen(line ?? _defaultForeground), x, y, w, h);
var r = Get2DRenderer(surfaceID);
var w = Math.Max(width, 1);
var h = Math.Max(height, 1);
r.DrawRectangle(line ?? _defaultForeground, x, y, w, h);
var bg = background ?? _defaultBackground;
if (bg != null) g.FillRectangle(GetBrush(bg.Value), x + 1, y + 1, Math.Max(w - 1, 0), Math.Max(h - 1, 0));
if (bg != null) r.FillRectangle(bg.Value, x + 1, y + 1, Math.Max(w - 2, 0), Math.Max(h - 2, 0));
}
public void DrawString(int x, int y, string message, Color? forecolor = null, Color? backcolor = null, int? fontsize = null, string fontfamily = null, string fontstyle = null, string horizalign = null, string vertalign = null, DisplaySurfaceID? surfaceID = null)
@ -500,7 +399,7 @@ namespace BizHawk.Client.Common
_ => FontStyle.Regular
};
using var g = GetGraphics(surfaceID);
using var g = Graphics.FromImage(_nullGraphicsBitmap);
// The text isn't written out using GenericTypographic, so measuring it using GenericTypographic seemed to make it worse.
// And writing it out with GenericTypographic just made it uglier. :p
@ -535,17 +434,17 @@ namespace BizHawk.Client.Common
break;
}
var r = Get2DRenderer(surfaceID);
var bg = backcolor ?? _defaultBackground;
if (bg != null)
{
var brush = GetBrush(bg.Value);
for (var xd = -1; xd <= 1; xd++) for (var yd = -1; yd <= 1; yd++)
{
g.DrawString(message, font, brush, x + xd, y + yd);
r.DrawString(message, font, bg.Value, x + xd, y + yd);
}
}
g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
g.DrawString(message, font, GetBrush(forecolor ?? _defaultForeground), x, y);
r.DrawString(message, font, forecolor ?? _defaultForeground, x, y, textRenderingHint: TextRenderingHint.SingleBitPerPixelGridFit);
}
catch (Exception)
{
@ -594,12 +493,14 @@ namespace BizHawk.Client.Common
index = _defaultPixelFont;
break;
}
using var g = GetGraphics(surfaceID);
using var g = Graphics.FromImage(_nullGraphicsBitmap);
var font = new Font(_displayManager.CustomFonts.Families[index], 8, FontStyle.Regular, GraphicsUnit.Pixel);
var sizeOfText = g.MeasureString(message, font, width: 0, PixelTextFormat).ToSize();
if (backcolor.HasValue) g.FillRectangle(GetBrush(backcolor.Value), new Rectangle(new Point(x, y), sizeOfText + new Size(1, 0)));
g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
g.DrawString(message, font, GetBrush(forecolor ?? _defaultForeground), x + 1, y, PixelTextFormat);
var r = Get2DRenderer(surfaceID);
if (backcolor.HasValue) r.FillRectangle(backcolor.Value, x, y, sizeOfText.Width + 2, sizeOfText.Height);
r.DrawString(message, font, forecolor ?? _defaultForeground, x + 1, y, PixelTextFormat, TextRenderingHint.SingleBitPerPixelGridFit);
}
catch (Exception)
{
@ -632,17 +533,11 @@ namespace BizHawk.Client.Common
y -= oy;
}
var pos = new MessagePosition{ X = x, Y = y, Anchor = (MessagePosition.AnchorType)a };
var pos = new MessagePosition { X = x, Y = y, Anchor = (MessagePosition.AnchorType)a };
_displayManager.OSD.AddGuiText(message, pos, Color.Black, forecolor ?? Color.White);
}
public void Dispose()
{
UnlockSurface(DisplaySurfaceID.EmuCore);
UnlockSurface(DisplaySurfaceID.Client);
foreach (var brush in _solidBrushes.Values) brush.Dispose();
foreach (var brush in _pens.Values) brush.Dispose();
ClearImageCache();
}
=> ClearImageCache();
}
}

View File

@ -7,18 +7,21 @@ namespace BizHawk.Client.Common
public interface IGuiApi : IDisposable, IExternalApi
{
void ToggleCompositingMode();
[Obsolete("No longer supported, returns null always.")]
ImageAttributes GetAttributes();
[Obsolete("No longer supported, no-op.")]
void SetAttributes(ImageAttributes a);
void WithSurface(DisplaySurfaceID surfaceID, Action drawingCallsFunc);
[Obsolete]
[Obsolete("No longer supported, no-op.")]
void DrawNew(string name, bool clear = true);
[Obsolete]
[Obsolete("No longer supported, no-op.")]
void DrawFinish();
[Obsolete]
[Obsolete("Always true")]
bool HasGUISurface { get; }
void SetPadding(int all);
@ -32,7 +35,7 @@ namespace BizHawk.Client.Common
void ClearText();
void SetDefaultForegroundColor(Color color);
void SetDefaultBackgroundColor(Color color);
Color? GetDefaultTextBackground();
Color GetDefaultTextBackground();
void SetDefaultTextBackground(Color color);
void SetDefaultPixelFont(string fontfamily);
void DrawBezier(Point p1, Point p2, Point p3, Point p4, Color? color = null, DisplaySurfaceID? surfaceID = null);

View File

@ -12,7 +12,6 @@ using System.Runtime.InteropServices;
using BizHawk.Bizware.Graphics;
using BizHawk.Client.Common.FilterManager;
using BizHawk.Client.Common.Filters;
using BizHawk.Common.CollectionExtensions;
using BizHawk.Common.PathExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Consoles.Nintendo.N3DS;
@ -28,8 +27,6 @@ namespace BizHawk.Client.Common
/// </summary>
public abstract class DisplayManagerBase : IDisposable
{
private static DisplaySurface CreateDisplaySurface(int w, int h) => new(w, h);
protected class DisplayManagerRenderTargetProvider : IRenderTargetProvider
{
private readonly Func<Size, IRenderTarget> _callback;
@ -66,9 +63,6 @@ namespace BizHawk.Client.Common
_gl = gl;
_renderer = renderer;
// it's sort of important for these to be initialized to something nonzero
_currEmuWidth = _currEmuHeight = 1;
_videoTextureFrugalizer = new(_gl);
_shaderChainFrugalizers = new RenderTargetFrugalizer[16]; // hacky hardcoded limit.. need some other way to manage these
@ -110,10 +104,9 @@ namespace BizHawk.Client.Common
}
}
_apiHawkSurfaceSets[DisplaySurfaceID.EmuCore] = new(CreateDisplaySurface);
_apiHawkSurfaceSets[DisplaySurfaceID.Client] = new(CreateDisplaySurface);
_apiHawkSurfaceFrugalizers[DisplaySurfaceID.EmuCore] = new(_gl);
_apiHawkSurfaceFrugalizers[DisplaySurfaceID.Client] = new(_gl);
_imGuiResourceCache = new ImGuiResourceCache(_gl);
_apiHawkIDTo2DRenderer.Add(DisplaySurfaceID.EmuCore, _gl.Create2DRenderer(_imGuiResourceCache));
_apiHawkIDTo2DRenderer.Add(DisplaySurfaceID.Client, _gl.Create2DRenderer(_imGuiResourceCache));
RefreshUserShader();
}
@ -140,11 +133,14 @@ namespace BizHawk.Client.Common
ActivateOpenGLContext();
_videoTextureFrugalizer.Dispose();
foreach (var f in _apiHawkSurfaceFrugalizers.Values)
foreach (var r in _apiHawkIDTo2DRenderer.Values)
{
f.Dispose();
r.Dispose();
}
_imGuiResourceCache.Dispose();
foreach (var f in _shaderChainFrugalizers)
{
f?.Dispose();
@ -169,12 +165,6 @@ namespace BizHawk.Client.Common
// layer resources
protected FilterProgram _currentFilterProgram;
/// <summary>
/// these variables will track the dimensions of the last frame's (or the next frame? this is confusing) emulator native output size
/// THIS IS OLD JUNK. I should get rid of it, I think. complex results from the last filter ingestion should be saved instead.
/// </summary>
private int _currEmuWidth, _currEmuHeight;
/// <summary>
/// additional pixels added at the unscaled level for the use of lua drawing. essentially increases the input video provider dimensions
/// </summary>
@ -192,8 +182,6 @@ namespace BizHawk.Client.Common
private readonly TextureFrugalizer _videoTextureFrugalizer;
private readonly Dictionary<DisplaySurfaceID, TextureFrugalizer> _apiHawkSurfaceFrugalizers = new();
protected readonly RenderTargetFrugalizer[] _shaderChainFrugalizers;
private readonly RetroShaderChain _shaderChainHq2X;
@ -402,16 +390,9 @@ namespace BizHawk.Client.Common
private void AppendApiHawkLayer(FilterProgram chain, DisplaySurfaceID surfaceID)
{
var luaNativeSurface = _apiHawkSurfaceSets[surfaceID].GetCurrent();
if (luaNativeSurface == null)
{
return;
}
var luaNativeTexture = _apiHawkSurfaceFrugalizers[surfaceID].Get(luaNativeSurface);
var fLuaLayer = new LuaLayer();
fLuaLayer.SetTexture(luaNativeTexture);
chain.AddFilter(fLuaLayer, surfaceID.GetName());
var apiHawkRenderer = _apiHawkIDTo2DRenderer[surfaceID];
var fApiHawkLayer = new ApiHawkLayer(apiHawkRenderer);
chain.AddFilter(fApiHawkLayer, surfaceID.GetName());
}
protected abstract Point GraphicsControlPointToClient(Point p);
@ -815,10 +796,6 @@ namespace BizHawk.Client.Common
}
}
// record the size of what we received, since lua and stuff is gonna want to draw onto it
_currEmuWidth = bufferWidth;
_currEmuHeight = bufferHeight;
//build the default filter chain and set it up with services filters will need
var chainInsize = new Size(bufferWidth, bufferHeight);
@ -930,87 +907,23 @@ namespace BizHawk.Client.Common
}
}
private readonly Dictionary<DisplaySurfaceID, IDisplaySurface> _apiHawkIDToSurface = new();
/// <remarks>Can't this just be a prop of <see cref="IDisplaySurface"/>? --yoshi</remarks>
private readonly Dictionary<IDisplaySurface, DisplaySurfaceID> _apiHawkSurfaceToID = new();
private readonly Dictionary<DisplaySurfaceID, SwappableDisplaySurfaceSet<DisplaySurface>> _apiHawkSurfaceSets = new();
private readonly ImGuiResourceCache _imGuiResourceCache;
private readonly Dictionary<DisplaySurfaceID, I2DRenderer> _apiHawkIDTo2DRenderer = new();
/// <summary>
/// Peeks a locked lua surface, or returns null if it isn't locked
/// Gets an ApiHawk 2D renderer, suitable for drawing with Gui/lua apis and such
/// The size of this surface might change between different calls
/// Implicitly, if the size changes the surface will be cleared
/// </summary>
public IDisplaySurface PeekApiHawkLockedSurface(DisplaySurfaceID surfaceID)
=> _apiHawkIDToSurface.TryGetValue(surfaceID, out var surface) ? surface : null;
public IDisplaySurface LockApiHawkSurface(DisplaySurfaceID surfaceID, bool clear)
{
if (_apiHawkIDToSurface.ContainsKey(surfaceID))
{
throw new InvalidOperationException($"ApiHawk/Lua surface is already locked: {surfaceID.GetName()}");
}
var sdss = _apiHawkSurfaceSets.GetValueOrPut(surfaceID, static _ => new(CreateDisplaySurface));
// placeholder logic for more abstracted surface definitions from filter chain
var (currNativeWidth, currNativeHeight) = GetPanelNativeSize();
currNativeWidth += ClientExtraPadding.Left + ClientExtraPadding.Right;
currNativeHeight += ClientExtraPadding.Top + ClientExtraPadding.Bottom;
var (width, height) = surfaceID switch
{
DisplaySurfaceID.EmuCore => (GameExtraPadding.Left + _currEmuWidth + GameExtraPadding.Right, GameExtraPadding.Top + _currEmuHeight + GameExtraPadding.Bottom),
DisplaySurfaceID.Client => (currNativeWidth, currNativeHeight),
_ => throw new InvalidOperationException()
};
IDisplaySurface ret = sdss.AllocateSurface(width, height, clear);
_apiHawkIDToSurface[surfaceID] = ret;
_apiHawkSurfaceToID[ret] = surfaceID;
return ret;
}
public I2DRenderer GetApiHawk2DRenderer(DisplaySurfaceID surfaceID)
=> _apiHawkIDTo2DRenderer[surfaceID];
public void ClearApiHawkSurfaces()
{
foreach (var kvp in _apiHawkSurfaceSets)
foreach (var renderer in _apiHawkIDTo2DRenderer.Values)
{
try
{
if (PeekApiHawkLockedSurface(kvp.Key) == null)
{
var surfLocked = LockApiHawkSurface(kvp.Key, true);
if (surfLocked != null)
{
UnlockApiHawkSurface(surfLocked);
}
}
_apiHawkSurfaceSets[kvp.Key].SetPending(null);
}
catch (InvalidOperationException)
{
// ignored
}
renderer.Clear();
}
}
/// <summary>unlocks this IDisplaySurface which had better have been locked as a lua surface</summary>
/// <exception cref="InvalidOperationException">already unlocked</exception>
public void UnlockApiHawkSurface(IDisplaySurface surface)
{
if (surface is not DisplaySurface dispSurfaceImpl)
{
throw new ArgumentException("don't mix " + nameof(IDisplaySurface) + " implementations!", nameof(surface));
}
if (!_apiHawkSurfaceToID.TryGetValue(dispSurfaceImpl, out var surfaceID))
{
throw new InvalidOperationException("Surface was not locked as a lua surface");
}
_apiHawkSurfaceToID.Remove(dispSurfaceImpl);
_apiHawkIDToSurface.Remove(surfaceID);
_apiHawkSurfaceSets[surfaceID].SetPending(dispSurfaceImpl);
}
}
}

View File

@ -1,63 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
namespace BizHawk.Client.Common
{
/// <summary>
/// This is a wrapper for a Bitmap.
/// It should be phased out, in favor of BitmapBuffer and Texture2d's
/// </summary>
public unsafe class DisplaySurface : IDisplaySurface
{
private const PixelFormat Format = PixelFormat.Format32bppArgb;
private Bitmap _bmp;
public void Clear()
{
var bmpData = _bmp.LockBits(new(0, 0, Width, Height), ImageLockMode.WriteOnly, Format);
new Span<byte>((void*)bmpData.Scan0, bmpData.Stride * bmpData.Height).Clear();
_bmp.UnlockBits(bmpData);
}
public Bitmap PeekBitmap()
{
return _bmp;
}
public Graphics GetGraphics()
{
return Graphics.FromImage(_bmp);
}
public DisplaySurface(int width, int height)
{
// can't create a bitmap with zero dimensions, so for now, just bump it up to one
if (width == 0)
{
width = 1;
}
if (height == 0)
{
height = 1;
}
Width = width;
Height = height;
_bmp = new Bitmap(Width, Height, Format);
}
public int Width { get; }
public int Height { get; }
public void Dispose()
{
_bmp?.Dispose();
_bmp = null;
}
}
}

View File

@ -525,9 +525,13 @@ namespace BizHawk.Client.Common.Filters
}
}
/// <remarks>More accurately, ApiHawkLayer, since the <c>gui</c> Lua library is delegated.</remarks>
public class LuaLayer : BaseFilter
public class ApiHawkLayer : BaseFilter
{
private readonly I2DRenderer _renderer;
public ApiHawkLayer(I2DRenderer renderer)
=> _renderer = renderer;
public override void Initialize()
{
DeclareInput(SurfaceDisposition.RenderTarget);
@ -538,19 +542,26 @@ namespace BizHawk.Client.Common.Filters
DeclareOutput(state);
}
private ITexture2D _texture;
public void SetTexture(ITexture2D tex)
{
_texture = tex;
}
public override void Run()
{
var outSize = FindOutput().SurfaceFormat.Size;
var output = _renderer.Render(outSize.Width, outSize.Height);
// render target might have changed when rendering, rebind the filter chain's target
var rt = FilterProgram.CurrRenderTarget;
if (rt == null)
{
FilterProgram.GL.BindDefaultRenderTarget();
}
else
{
rt.Bind();
}
FilterProgram.GuiRenderer.Begin(outSize);
FilterProgram.GuiRenderer.EnableBlending();
FilterProgram.GuiRenderer.Draw(_texture);
FilterProgram.GuiRenderer.Draw(output);
FilterProgram.GuiRenderer.End();
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Drawing;
namespace BizHawk.Client.Common
{
public interface IDisplaySurface : IDisposable
{
int Height { get; }
int Width { get; }
void Clear();
/// <returns>a <see cref="Graphics"/> used to render to this surface; be sure to dispose it!</returns>
Graphics GetGraphics();
Bitmap PeekBitmap();
}
}

View File

@ -1,87 +0,0 @@
using System;
using System.Collections.Generic;
namespace BizHawk.Client.Common
{
/// <summary>
/// encapsulates thread-safe concept of pending/current display surfaces, reusing buffers where matching
/// sizes are available and keeping them cleaned up when they don't seem like they'll need to be used anymore
/// </summary>
public class SwappableDisplaySurfaceSet<T>
where T : class, IDisplaySurface
{
private readonly Func<int, int, T> _createDispSurface;
private T _pending, _current;
private bool _isPending;
private readonly Queue<T> _releasedSurfaces = new();
public SwappableDisplaySurfaceSet(Func<int, int, T> createDispSurface) => _createDispSurface = createDispSurface;
/// <summary>
/// retrieves a surface with the specified size, reusing an old buffer if available and clearing if requested
/// </summary>
public T AllocateSurface(int width, int height, bool needsClear = true)
{
for (; ; )
{
T trial;
lock (this)
{
if (_releasedSurfaces.Count == 0) break;
trial = _releasedSurfaces.Dequeue();
}
if (trial.Width == width && trial.Height == height)
{
if (needsClear)
{
trial.Clear();
}
return trial;
}
trial.Dispose();
}
return _createDispSurface(width, height);
}
/// <summary>
/// sets the provided buffer as pending. takes control of the supplied buffer
/// </summary>
public void SetPending(T newPending)
{
lock (this)
{
if (_pending != null) _releasedSurfaces.Enqueue(_pending);
_pending = newPending;
_isPending = true;
}
}
public void ReleaseSurface(T surface)
{
lock (this) _releasedSurfaces.Enqueue(surface);
}
/// <summary>
/// returns the current buffer, making the most recent pending buffer (if there is such) as the new current first.
/// </summary>
public T GetCurrent()
{
lock (this)
{
if (_isPending)
{
if (_current != null) _releasedSurfaces.Enqueue(_current);
_current = _pending;
_pending = null;
_isPending = false;
}
}
return _current;
}
}
}

View File

@ -6,7 +6,7 @@ using BizHawk.Bizware.Graphics;
namespace BizHawk.Client.Common
{
/// <summary>
/// Recycles a pair of temporary textures (in case double-buffering helps any) to contain a BitmapBuffer's or DisplaySurface's contents, as long as the dimensions match.
/// Recycles a pair of temporary textures (in case double-buffering helps any) to contain a BitmapBuffer's contents, as long as the dimensions match.
/// When the dimensions don't match, a new one will be allocated
/// </summary>
public class TextureFrugalizer : IDisposable
@ -35,12 +35,6 @@ namespace BizHawk.Client.Common
private readonly IGL _gl;
private List<ITexture2D> _currentTextures;
public ITexture2D Get(IDisplaySurface ds)
{
using var bb = new BitmapBuffer(ds.PeekBitmap(), new());
return Get(bb);
}
public ITexture2D Get(BitmapBuffer bb)
{
//get the current entry

View File

@ -21,6 +21,7 @@ namespace BizHawk.Client.Common
=> DisplaySurfaceIDParser.Parse(surfaceName) ?? _rememberedSurfaceID;
#pragma warning disable CS0612
#pragma warning disable CS0618
[LuaDeprecatedMethod]
[LuaMethod("DrawNew", "Changes drawing target to the specified lua surface name. This may clobber any previous drawing to this surface (pass false if you don't want it to)")]
public void DrawNew(string name, bool? clear = true)
@ -31,6 +32,7 @@ namespace BizHawk.Client.Common
public void DrawFinish()
=> APIs.Gui.DrawFinish();
#pragma warning restore CS0612
#pragma warning restore CS0618
[LuaMethodExample("gui.addmessage( \"Some message\" );")]
[LuaMethod("addmessage", "Adds a message to the OSD's message area")]
@ -92,7 +94,7 @@ namespace BizHawk.Client.Common
}
catch (Exception)
{
return;
// ignored
}
}
@ -232,7 +234,7 @@ namespace BizHawk.Client.Common
}
catch (Exception)
{
return;
// ignored
}
}
@ -313,7 +315,7 @@ namespace BizHawk.Client.Common
[LuaColorParam] object backcolor = null,
string fontfamily = null,
string surfaceName = null)
=> APIs.Gui.PixelText(x, y, message, _th.SafeParseColor(forecolor), _th.SafeParseColor(backcolor) ?? APIs.Gui.GetDefaultTextBackground().Value, fontfamily, surfaceID: UseOrFallback(surfaceName));
=> APIs.Gui.PixelText(x, y, message, _th.SafeParseColor(forecolor), _th.SafeParseColor(backcolor) ?? APIs.Gui.GetDefaultTextBackground(), fontfamily, surfaceID: UseOrFallback(surfaceName));
[LuaMethodExample("gui.text( 16, 32, \"Some message\", 0x7F0000FF, \"bottomleft\" );")]
[LuaMethod("text", "Displays the given text on the screen at the given coordinates. Optional Foreground color. The optional anchor flag anchors the text to one of the four corners. Anchor flag parameters: topleft, topright, bottomleft, bottomright")]

View File

@ -97,7 +97,6 @@ namespace BizHawk.Client.EmuHawk
{
_luaContainer?.Dispose();
_luaContainer = Register(serviceProvider, logCallback, mainForm, displayManager, inputManager, movieSession, toolManager, config, emulator, game);
((GuiApi) _luaContainer.Gui).EnableLuaAutolockHack = true;
return _luaContainer;
}
}

View File

@ -252,7 +252,7 @@ namespace BizHawk.Client.EmuHawk
return TryInitIGL(initialConfig.DispMethod = method);
}
// need to have a context active for checking renderer, will be disposed afterwards
using (new SDL2OpenGLContext(3, 0, true, false))
using (new SDL2OpenGLContext(3, 2, true, false))
{
using var testIgl = new IGL_OpenGL();
_ = CheckRenderer(testIgl);

View File

@ -205,8 +205,6 @@ namespace BizHawk.Client.EmuHawk
public void CallSaveStateEvent(string name)
{
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
try
{
foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_SAVESTATE).ToList())
@ -222,8 +220,6 @@ namespace BizHawk.Client.EmuHawk
public void CallLoadStateEvent(string name)
{
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
try
{
foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_LOADSTATE).ToList())
@ -241,8 +237,6 @@ namespace BizHawk.Client.EmuHawk
{
if (IsUpdateSupressed) return;
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
try
{
foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_PREFRAME).ToList())
@ -260,8 +254,6 @@ namespace BizHawk.Client.EmuHawk
{
if (IsUpdateSupressed) return;
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
try
{
foreach (var lf in RegisteredFunctions.Where(static l => l.Event == NamedLuaFunction.EVENT_TYPE_POSTFRAME).ToList())
@ -277,8 +269,6 @@ namespace BizHawk.Client.EmuHawk
public void CallExitEvent(LuaFile lf)
{
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
foreach (var exitCallback in RegisteredFunctions
.Where(l => l.Event == NamedLuaFunction.EVENT_TYPE_ENGINESTOP
&& (l.LuaFile.Path == lf.Path || ReferenceEquals(l.LuaFile.Thread, lf.Thread)))
@ -340,7 +330,6 @@ namespace BizHawk.Client.EmuHawk
public (bool WaitForFrame, bool Terminated) ResumeScript(LuaFile lf)
{
_currThread = lf.Thread;
using var luaAutoUnlockHack = GuiAPI.ThisIsTheLuaAutoUnlockHack();
try
{