Added thread-affinity checks to SDL2 renderer.

If a call is made into a graphics resource that has thread-affinity, from a thread other than the one that created the graphics device, an exception will now be thrown to make debugging easier.
This commit is contained in:
RoosterDragon
2015-05-20 20:52:02 +01:00
parent 4fa20e78fa
commit fc436f1aab
11 changed files with 145 additions and 40 deletions

View File

@@ -14,7 +14,7 @@ using OpenTK.Graphics.OpenGL;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public static class ErrorHandler static class ErrorHandler
{ {
public static void CheckGlVersion() public static void CheckGlVersion()
{ {

View File

@@ -16,10 +16,10 @@ using OpenTK.Graphics.OpenGL;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public sealed class FrameBuffer : IFrameBuffer sealed class FrameBuffer : ThreadAffine, IFrameBuffer
{ {
Texture texture; readonly Texture texture;
Size size; readonly Size size;
int framebuffer, depth; int framebuffer, depth;
bool disposed; bool disposed;
@@ -83,6 +83,8 @@ namespace OpenRA.Platforms.Default
int[] cv = new int[4]; int[] cv = new int[4];
public void Bind() public void Bind()
{ {
VerifyThreadAffinity();
// Cache viewport rect to restore when unbinding // Cache viewport rect to restore when unbinding
cv = ViewportRectangle(); cv = ViewportRectangle();
@@ -100,6 +102,7 @@ namespace OpenRA.Platforms.Default
public void Unbind() public void Unbind()
{ {
VerifyThreadAffinity();
GL.Flush(); GL.Flush();
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0);
@@ -108,7 +111,14 @@ namespace OpenRA.Platforms.Default
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
} }
public ITexture Texture { get { return texture; } } public ITexture Texture
{
get
{
VerifyThreadAffinity();
return texture;
}
}
~FrameBuffer() ~FrameBuffer()
{ {

View File

@@ -13,7 +13,7 @@ using OpenRA.Primitives;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public static class MultiTapDetection static class MultiTapDetection
{ {
static Cache<Keycode, TapHistory> keyHistoryCache = static Cache<Keycode, TapHistory> keyHistoryCache =
new Cache<Keycode, TapHistory>(_ => new TapHistory(DateTime.Now - TimeSpan.FromSeconds(1))); new Cache<Keycode, TapHistory>(_ => new TapHistory(DateTime.Now - TimeSpan.FromSeconds(1)));

View File

@@ -50,6 +50,7 @@
<Compile Include="FrameBuffer.cs" /> <Compile Include="FrameBuffer.cs" />
<Compile Include="MultiTapDetection.cs" /> <Compile Include="MultiTapDetection.cs" />
<Compile Include="Texture.cs" /> <Compile Include="Texture.cs" />
<Compile Include="ThreadAffine.cs" />
<Compile Include="VertexBuffer.cs" /> <Compile Include="VertexBuffer.cs" />
<Compile Include="OpenAlSoundEngine.cs" /> <Compile Include="OpenAlSoundEngine.cs" />
</ItemGroup> </ItemGroup>

View File

@@ -12,6 +12,7 @@ using System;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using OpenRA; using OpenRA;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenTK.Graphics; using OpenTK.Graphics;
@@ -20,20 +21,18 @@ using SDL2;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public sealed class Sdl2GraphicsDevice : IGraphicsDevice sealed class Sdl2GraphicsDevice : ThreadAffine, IGraphicsDevice
{ {
Size size; readonly Sdl2Input input;
Sdl2Input input;
IntPtr context, window; IntPtr context, window;
bool disposed; bool disposed;
public Size WindowSize { get { return size; } } public Size WindowSize { get; private set; }
public Sdl2GraphicsDevice(Size windowSize, WindowMode windowMode) public Sdl2GraphicsDevice(Size windowSize, WindowMode windowMode)
{ {
Console.WriteLine("Using SDL 2 with OpenGL renderer"); Console.WriteLine("Using SDL 2 with OpenGL renderer");
WindowSize = windowSize;
size = windowSize;
SDL.SDL_Init(SDL.SDL_INIT_NOPARACHUTE | SDL.SDL_INIT_VIDEO); SDL.SDL_Init(SDL.SDL_INIT_NOPARACHUTE | SDL.SDL_INIT_VIDEO);
SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_DOUBLEBUFFER, 1); SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_DOUBLEBUFFER, 1);
@@ -46,15 +45,16 @@ namespace OpenRA.Platforms.Default
SDL.SDL_GetCurrentDisplayMode(0, out display); SDL.SDL_GetCurrentDisplayMode(0, out display);
Console.WriteLine("Desktop resolution: {0}x{1}", display.w, display.h); Console.WriteLine("Desktop resolution: {0}x{1}", display.w, display.h);
if (size.Width == 0 && size.Height == 0) if (WindowSize.Width == 0 && WindowSize.Height == 0)
{ {
Console.WriteLine("No custom resolution provided, using desktop resolution"); Console.WriteLine("No custom resolution provided, using desktop resolution");
size = new Size(display.w, display.h); WindowSize = new Size(display.w, display.h);
} }
Console.WriteLine("Using resolution: {0}x{1}", size.Width, size.Height); Console.WriteLine("Using resolution: {0}x{1}", WindowSize.Width, WindowSize.Height);
window = SDL.SDL_CreateWindow("OpenRA", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, size.Width, size.Height, SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL); window = SDL.SDL_CreateWindow("OpenRA", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
WindowSize.Width, WindowSize.Height, SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL);
if (Game.Settings.Game.LockMouseWindow) if (Game.Settings.Game.LockMouseWindow)
GrabWindowMouseFocus(); GrabWindowMouseFocus();
@@ -100,6 +100,7 @@ namespace OpenRA.Platforms.Default
public IHardwareCursor CreateHardwareCursor(string name, Size size, byte[] data, int2 hotspot) public IHardwareCursor CreateHardwareCursor(string name, Size size, byte[] data, int2 hotspot)
{ {
VerifyThreadAffinity();
try try
{ {
return new SDL2HardwareCursor(size, data, hotspot); return new SDL2HardwareCursor(size, data, hotspot);
@@ -112,6 +113,7 @@ namespace OpenRA.Platforms.Default
public void SetHardwareCursor(IHardwareCursor cursor) public void SetHardwareCursor(IHardwareCursor cursor)
{ {
VerifyThreadAffinity();
var c = cursor as SDL2HardwareCursor; var c = cursor as SDL2HardwareCursor;
if (c == null) if (c == null)
SDL.SDL_ShowCursor((int)SDL.SDL_bool.SDL_FALSE); SDL.SDL_ShowCursor((int)SDL.SDL_bool.SDL_FALSE);
@@ -214,12 +216,14 @@ namespace OpenRA.Platforms.Default
public void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices) public void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices)
{ {
VerifyThreadAffinity();
GL.DrawArrays(ModeFromPrimitiveType(pt), firstVertex, numVertices); GL.DrawArrays(ModeFromPrimitiveType(pt), firstVertex, numVertices);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
} }
public void Clear() public void Clear()
{ {
VerifyThreadAffinity();
GL.ClearColor(0, 0, 0, 1); GL.ClearColor(0, 0, 0, 1);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.Clear(ClearBufferMask.ColorBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit);
@@ -228,6 +232,7 @@ namespace OpenRA.Platforms.Default
public void EnableDepthBuffer() public void EnableDepthBuffer()
{ {
VerifyThreadAffinity();
GL.Clear(ClearBufferMask.DepthBufferBit); GL.Clear(ClearBufferMask.DepthBufferBit);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.Enable(EnableCap.DepthTest); GL.Enable(EnableCap.DepthTest);
@@ -236,12 +241,14 @@ namespace OpenRA.Platforms.Default
public void DisableDepthBuffer() public void DisableDepthBuffer()
{ {
VerifyThreadAffinity();
GL.Disable(EnableCap.DepthTest); GL.Disable(EnableCap.DepthTest);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
} }
public void SetBlendMode(BlendMode mode) public void SetBlendMode(BlendMode mode)
{ {
VerifyThreadAffinity();
GL.BlendEquation(BlendEquationMode.FuncAdd); GL.BlendEquation(BlendEquationMode.FuncAdd);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
@@ -290,23 +297,27 @@ namespace OpenRA.Platforms.Default
public void GrabWindowMouseFocus() public void GrabWindowMouseFocus()
{ {
VerifyThreadAffinity();
SDL.SDL_SetWindowGrab(window, SDL.SDL_bool.SDL_TRUE); SDL.SDL_SetWindowGrab(window, SDL.SDL_bool.SDL_TRUE);
} }
public void ReleaseWindowMouseFocus() public void ReleaseWindowMouseFocus()
{ {
VerifyThreadAffinity();
SDL.SDL_SetWindowGrab(window, SDL.SDL_bool.SDL_FALSE); SDL.SDL_SetWindowGrab(window, SDL.SDL_bool.SDL_FALSE);
} }
public void EnableScissor(int left, int top, int width, int height) public void EnableScissor(int left, int top, int width, int height)
{ {
VerifyThreadAffinity();
if (width < 0) if (width < 0)
width = 0; width = 0;
if (height < 0) if (height < 0)
height = 0; height = 0;
GL.Scissor(left, size.Height - (top + height), width, height); GL.Scissor(left, WindowSize.Height - (top + height), width, height);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.Enable(EnableCap.ScissorTest); GL.Enable(EnableCap.ScissorTest);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
@@ -314,19 +325,21 @@ namespace OpenRA.Platforms.Default
public void DisableScissor() public void DisableScissor()
{ {
VerifyThreadAffinity();
GL.Disable(EnableCap.ScissorTest); GL.Disable(EnableCap.ScissorTest);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
} }
public void SetLineWidth(float width) public void SetLineWidth(float width)
{ {
VerifyThreadAffinity();
GL.LineWidth(width); GL.LineWidth(width);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
} }
public Bitmap TakeScreenshot() public Bitmap TakeScreenshot()
{ {
var rect = new Rectangle(Point.Empty, size); var rect = new Rectangle(Point.Empty, WindowSize);
var bitmap = new Bitmap(rect.Width, rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); var bitmap = new Bitmap(rect.Width, rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
var data = bitmap.LockBits(rect, var data = bitmap.LockBits(rect,
System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
@@ -349,13 +362,52 @@ namespace OpenRA.Platforms.Default
return bitmap; return bitmap;
} }
public void Present() { SDL.SDL_GL_SwapWindow(window); } public void Present()
public void PumpInput(IInputHandler inputHandler) { input.PumpInput(inputHandler); } {
public string GetClipboardText() { return input.GetClipboardText(); } VerifyThreadAffinity();
public IVertexBuffer<Vertex> CreateVertexBuffer(int size) { return new VertexBuffer<Vertex>(size); } SDL.SDL_GL_SwapWindow(window);
public ITexture CreateTexture() { return new Texture(); } }
public ITexture CreateTexture(Bitmap bitmap) { return new Texture(bitmap); }
public IFrameBuffer CreateFrameBuffer(Size s) { return new FrameBuffer(s); } public void PumpInput(IInputHandler inputHandler)
public IShader CreateShader(string name) { return new Shader(name); } {
VerifyThreadAffinity();
input.PumpInput(inputHandler);
}
public string GetClipboardText()
{
VerifyThreadAffinity();
return input.GetClipboardText();
}
public IVertexBuffer<Vertex> CreateVertexBuffer(int size)
{
VerifyThreadAffinity();
return new VertexBuffer<Vertex>(size);
}
public ITexture CreateTexture()
{
VerifyThreadAffinity();
return new Texture();
}
public ITexture CreateTexture(Bitmap bitmap)
{
VerifyThreadAffinity();
return new Texture(bitmap);
}
public IFrameBuffer CreateFrameBuffer(Size s)
{
VerifyThreadAffinity();
return new FrameBuffer(s);
}
public IShader CreateShader(string name)
{
VerifyThreadAffinity();
return new Shader(name);
}
} }
} }

View File

@@ -15,7 +15,7 @@ using SDL2;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public class Sdl2Input class Sdl2Input
{ {
MouseButton lastButtonBits = (MouseButton)0; MouseButton lastButtonBits = (MouseButton)0;

View File

@@ -17,11 +17,11 @@ using OpenTK.Graphics.OpenGL;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public class Shader : IShader class Shader : ThreadAffine, IShader
{ {
readonly Dictionary<string, int> samplers = new Dictionary<string, int>(); readonly Dictionary<string, int> samplers = new Dictionary<string, int>();
readonly Dictionary<int, ITexture> textures = new Dictionary<int, ITexture>(); readonly Dictionary<int, ITexture> textures = new Dictionary<int, ITexture>();
int program; readonly int program;
protected int CompileShaderObject(ShaderType type, string name) protected int CompileShaderObject(ShaderType type, string name)
{ {
@@ -122,6 +122,7 @@ namespace OpenRA.Platforms.Default
public void Render(Action a) public void Render(Action a)
{ {
VerifyThreadAffinity();
GL.UseProgram(program); GL.UseProgram(program);
// bind the textures // bind the textures
@@ -138,6 +139,7 @@ namespace OpenRA.Platforms.Default
public void SetTexture(string name, ITexture t) public void SetTexture(string name, ITexture t)
{ {
VerifyThreadAffinity();
if (t == null) if (t == null)
return; return;
@@ -148,6 +150,7 @@ namespace OpenRA.Platforms.Default
public void SetVec(string name, float x) public void SetVec(string name, float x)
{ {
VerifyThreadAffinity();
GL.UseProgram(program); GL.UseProgram(program);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
var param = GL.GetUniformLocation(program, name); var param = GL.GetUniformLocation(program, name);
@@ -158,6 +161,7 @@ namespace OpenRA.Platforms.Default
public void SetVec(string name, float x, float y) public void SetVec(string name, float x, float y)
{ {
VerifyThreadAffinity();
GL.UseProgram(program); GL.UseProgram(program);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
var param = GL.GetUniformLocation(program, name); var param = GL.GetUniformLocation(program, name);
@@ -168,6 +172,7 @@ namespace OpenRA.Platforms.Default
public void SetVec(string name, float[] vec, int length) public void SetVec(string name, float[] vec, int length)
{ {
VerifyThreadAffinity();
var param = GL.GetUniformLocation(program, name); var param = GL.GetUniformLocation(program, name);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
switch (length) switch (length)
@@ -184,6 +189,7 @@ namespace OpenRA.Platforms.Default
public void SetMatrix(string name, float[] mtx) public void SetMatrix(string name, float[] mtx)
{ {
VerifyThreadAffinity();
if (mtx.Length != 16) if (mtx.Length != 16)
throw new InvalidDataException("Invalid 4x4 matrix"); throw new InvalidDataException("Invalid 4x4 matrix");

View File

@@ -16,15 +16,13 @@ using OpenTK.Graphics.OpenGL;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public sealed class Texture : ITexture sealed class Texture : ThreadAffine, ITexture
{ {
int texture; int texture;
TextureScaleFilter scaleFilter; TextureScaleFilter scaleFilter;
Size size;
public int ID { get { return texture; } } public int ID { get { return texture; } }
public Size Size { get; private set; }
public Size Size { get { return size; } }
bool disposed; bool disposed;
@@ -37,6 +35,7 @@ namespace OpenRA.Platforms.Default
set set
{ {
VerifyThreadAffinity();
if (scaleFilter == value) if (scaleFilter == value)
return; return;
@@ -83,10 +82,11 @@ namespace OpenRA.Platforms.Default
public void SetData(byte[] colors, int width, int height) public void SetData(byte[] colors, int width, int height)
{ {
VerifyThreadAffinity();
if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height)) if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height))
throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height)); throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height));
size = new Size(width, height); Size = new Size(width, height);
unsafe unsafe
{ {
fixed (byte* ptr = &colors[0]) fixed (byte* ptr = &colors[0])
@@ -103,13 +103,14 @@ namespace OpenRA.Platforms.Default
// An array of RGBA // An array of RGBA
public void SetData(uint[,] colors) public void SetData(uint[,] colors)
{ {
VerifyThreadAffinity();
var width = colors.GetUpperBound(1) + 1; var width = colors.GetUpperBound(1) + 1;
var height = colors.GetUpperBound(0) + 1; var height = colors.GetUpperBound(0) + 1;
if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height)) if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height))
throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height)); throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height));
size = new Size(width, height); Size = new Size(width, height);
unsafe unsafe
{ {
fixed (uint* ptr = &colors[0, 0]) fixed (uint* ptr = &colors[0, 0])
@@ -125,6 +126,7 @@ namespace OpenRA.Platforms.Default
public void SetData(Bitmap bitmap) public void SetData(Bitmap bitmap)
{ {
VerifyThreadAffinity();
var allocatedBitmap = false; var allocatedBitmap = false;
if (!Exts.IsPowerOf2(bitmap.Width) || !Exts.IsPowerOf2(bitmap.Height)) if (!Exts.IsPowerOf2(bitmap.Width) || !Exts.IsPowerOf2(bitmap.Height))
{ {
@@ -134,7 +136,7 @@ namespace OpenRA.Platforms.Default
try try
{ {
size = new Size(bitmap.Width, bitmap.Height); Size = new Size(bitmap.Width, bitmap.Height);
var bits = bitmap.LockBits(bitmap.Bounds(), var bits = bitmap.LockBits(bitmap.Bounds(),
ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
@@ -153,7 +155,8 @@ namespace OpenRA.Platforms.Default
public byte[] GetData() public byte[] GetData()
{ {
var data = new byte[4 * size.Width * size.Height]; VerifyThreadAffinity();
var data = new byte[4 * Size.Width * Size.Height];
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, texture); GL.BindTexture(TextureTarget.Texture2D, texture);
@@ -172,10 +175,11 @@ namespace OpenRA.Platforms.Default
public void SetEmpty(int width, int height) public void SetEmpty(int width, int height)
{ {
VerifyThreadAffinity();
if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height)) if (!Exts.IsPowerOf2(width) || !Exts.IsPowerOf2(height))
throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height)); throw new InvalidDataException("Non-power-of-two array {0}x{1}".F(width, height));
size = new Size(width, height); Size = new Size(width, height);
PrepareTexture(); PrepareTexture();
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, width, height, GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, width, height,
0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero); 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero);

View File

@@ -0,0 +1,31 @@
#region Copyright & License Information
/*
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Threading;
namespace OpenRA.Platforms.Default
{
abstract class ThreadAffine
{
readonly int managedThreadId;
protected ThreadAffine()
{
managedThreadId = Thread.CurrentThread.ManagedThreadId;
}
protected void VerifyThreadAffinity()
{
if (managedThreadId != Thread.CurrentThread.ManagedThreadId)
throw new InvalidOperationException("Cross-thread operation not valid: This method must be called from the same thread that created this object.");
}
}
}

View File

@@ -14,7 +14,7 @@ using OpenTK.Graphics.OpenGL;
namespace OpenRA.Platforms.Default namespace OpenRA.Platforms.Default
{ {
public sealed class VertexBuffer<T> : IVertexBuffer<T> sealed class VertexBuffer<T> : ThreadAffine, IVertexBuffer<T>
where T : struct where T : struct
{ {
static readonly int VertexSize = Marshal.SizeOf(typeof(T)); static readonly int VertexSize = Marshal.SizeOf(typeof(T));
@@ -60,6 +60,7 @@ namespace OpenRA.Platforms.Default
public void Bind() public void Bind()
{ {
VerifyThreadAffinity();
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
ErrorHandler.CheckGlError(); ErrorHandler.CheckGlError();
GL.VertexPointer(3, VertexPointerType.Float, VertexSize, IntPtr.Zero); GL.VertexPointer(3, VertexPointerType.Float, VertexSize, IntPtr.Zero);

View File

@@ -16,7 +16,7 @@ namespace OpenRA.Platforms.Null
{ {
public sealed class NullGraphicsDevice : IGraphicsDevice public sealed class NullGraphicsDevice : IGraphicsDevice
{ {
public Size WindowSize { get; internal set; } public Size WindowSize { get; private set; }
public NullGraphicsDevice(Size size, WindowMode window) public NullGraphicsDevice(Size size, WindowMode window)
{ {