BizHawk/BizHawk.Client.EmuHawk/RenderPanel.cs

609 lines
17 KiB
C#

using System;
using System.Drawing;
using sysdrawingfont=System.Drawing.Font;
using sysdrawing2d=System.Drawing.Drawing2D;
using System.IO;
using System.Threading;
using System.Windows.Forms;
#if WINDOWS
using SlimDX;
using SlimDX.Direct3D9;
using d3d9font=SlimDX.Direct3D9.Font;
#endif
using BizHawk.Client.Common;
namespace BizHawk.Client.EmuHawk
{
#if WINDOWS
public class ImageTexture : IDisposable
{
public Device GraphicsDevice;
public Texture Texture;
private int imageWidth;
public int ImageWidth { get { return imageWidth; } }
private int imageHeight;
public int ImageHeight { get { return imageHeight; } }
private int textureWidth;
public int TextureWidth { get { return textureWidth; } }
private int textureHeight;
public int TextureHeight { get { return textureHeight; } }
public ImageTexture(Device graphicsDevice)
{
GraphicsDevice = graphicsDevice;
}
public unsafe void SetImage(DisplaySurface surface, int width, int height)
{
//this function fails if the width and height are zero
if (width == 0 || height == 0) return;
bool needsRecreating = false;
//experiment:
//needsRecreating = true;
if (Texture == null)
{
needsRecreating = true;
}
else
{
if (imageWidth != width || imageHeight != height)
{
needsRecreating = true;
}
}
// If we need to recreate the texture, do so.
if (needsRecreating)
{
if (Texture != null)
{
Texture.Dispose();
Texture = null;
}
// Copy the width/height to member fields.
imageWidth = width;
imageHeight = height;
// Round up the width/height to the nearest power of two.
textureWidth = 32; textureHeight = 32;
while (textureWidth < imageWidth) textureWidth <<= 1;
while (textureHeight < imageHeight) textureHeight <<= 1;
// Create a new texture instance.
Texture = new Texture(GraphicsDevice, textureWidth, textureHeight, 1, Usage.None, Format.A8R8G8B8, Pool.Managed);
}
surface.FromBitmap();
// Copy the image data to the texture.
using (var Data = Texture.LockRectangle(0, LockFlags.None).Data)
{
if (imageWidth == textureWidth)
{
// Widths are the same, just dump the data across (easy!)
Data.WriteRange(surface.PixelIntPtr, imageWidth * imageHeight << 2);
}
else
{
// Widths are different, need a bit of additional magic here to make them fit:
long RowSeekOffset = (textureWidth - imageWidth) << 2;
for (int r = 0, s = 0; r < imageHeight; ++r, s += imageWidth)
{
IntPtr src = new IntPtr(((byte*)surface.PixelPtr + r*surface.Stride));
Data.WriteRange(src,imageWidth << 2);
Data.Seek(RowSeekOffset, SeekOrigin.Current);
}
}
Texture.UnlockRectangle(0);
}
}
private bool disposed;
public void Dispose()
{
if (!disposed)
{
disposed = true;
if (Texture != null)
Texture.Dispose();
Texture = null;
GC.SuppressFinalize(this);
}
}
}
#endif
public interface IRenderer : IDisposable
{
void FastRenderAndPresent(DisplaySurface surface);
void Render(DisplaySurface surface);
void RenderOverlay(DisplaySurface surface);
void Clear(Color color);
void Present();
bool Resized { get; set; }
Size NativeSize { get; }
/// <summary>
/// convert coordinates
/// </summary>
/// <param name="p">desktop coordinates</param>
/// <returns>ivideoprovider coordinates</returns>
Point ScreenToScreen(Point p);
}
public class SysdrawingRenderPanel : IRenderer, IBlitter
{
private readonly sysdrawingfont MessageFont;
private readonly sysdrawingfont AlertFont;
private DisplaySurface tempBuffer;
private Graphics g;
private readonly SwappableDisplaySurfaceSet surfaceSet = new SwappableDisplaySurfaceSet();
public bool Resized { get; set; }
public void Dispose() { }
public void Render(DisplaySurface surface)
{
backingControl.ReleaseCallback = RetainedViewportPanelDisposeCallback;
lock (this)
tempBuffer = surfaceSet.AllocateSurface(backingControl.Width, backingControl.Height, false);
RenderInternal(surface);
}
class FontWrapper : IBlitterFont
{
public FontWrapper(sysdrawingfont font)
{
this.font = font;
}
public readonly sysdrawingfont font;
}
public Size NativeSize { get { return backingControl.ClientSize; } }
IBlitterFont IBlitter.GetFontType(string fontType)
{
if (fontType == "MessageFont") return new FontWrapper(MessageFont);
if (fontType == "AlertFont") return new FontWrapper(AlertFont);
return null;
}
void IBlitter.Open()
{
g = Graphics.FromImage(tempBuffer.PeekBitmap());
ClipBounds = new Rectangle(0, 0, NativeSize.Width, NativeSize.Height);
}
void IBlitter.Close()
{
g.Dispose();
}
void IBlitter.DrawString(string s, IBlitterFont font, Color color, float x, float y)
{
using (var brush = new SolidBrush(color))
g.DrawString(s, ((FontWrapper)font).font, brush, x, y);
}
SizeF IBlitter.MeasureString(string s, IBlitterFont _font)
{
var font = ((FontWrapper)_font).font;
return g.MeasureString(s, font);
}
public void Clear(Color color)
{
//todo
}
public Rectangle ClipBounds { get; set; }
bool RetainedViewportPanelDisposeCallback(Bitmap bmp)
{
lock (this)
{
DisplaySurface tempSurface = DisplaySurface.DisplaySurfaceWrappingBitmap(bmp);
surfaceSet.ReleaseSurface(tempSurface);
}
return false;
}
void RenderInternal(DisplaySurface surface, bool transparent = false)
{
using (var g = Graphics.FromImage(tempBuffer.PeekBitmap()))
{
g.PixelOffsetMode = sysdrawing2d.PixelOffsetMode.HighSpeed;
g.InterpolationMode = Global.Config.DispBlurry ? sysdrawing2d.InterpolationMode.Bilinear : sysdrawing2d.InterpolationMode.NearestNeighbor;
if (transparent) g.CompositingMode = sysdrawing2d.CompositingMode.SourceOver;
else g.CompositingMode = sysdrawing2d.CompositingMode.SourceCopy;
g.CompositingQuality = sysdrawing2d.CompositingQuality.HighSpeed;
if (backingControl.Width == surface.Width && backingControl.Height == surface.Height)
g.DrawImageUnscaled(surface.PeekBitmap(), 0, 0);
else
g.DrawImage(surface.PeekBitmap(), 0, 0, backingControl.Width, backingControl.Height);
}
if (!transparent)
{
lastsize = new Size(surface.Width, surface.Height);
}
}
public void FastRenderAndPresent(DisplaySurface surface)
{
backingControl.SetBitmap((Bitmap)surface.PeekBitmap().Clone());
}
public void RenderOverlay(DisplaySurface surface)
{
RenderInternal(surface, true);
}
public void Present()
{
backingControl.SetBitmap(tempBuffer.PeekBitmap());
tempBuffer = null;
}
public SysdrawingRenderPanel(RetainedViewportPanel control)
{
backingControl = control;
MessageFont = new sysdrawingfont("Courier", 14, FontStyle.Bold, GraphicsUnit.Pixel);
AlertFont = new sysdrawingfont("Courier", 14, FontStyle.Bold, GraphicsUnit.Pixel);
}
RetainedViewportPanel backingControl;
Size lastsize = new Size(256, 192);
public Point ScreenToScreen(Point p)
{
p = backingControl.PointToClient(p);
Point ret = new Point(p.X * lastsize.Width / backingControl.Width,
p.Y * lastsize.Height / backingControl.Height);
return ret;
}
}
public interface IBlitter
{
void Open();
void Close();
IBlitterFont GetFontType(string fontType);
void DrawString(string s, IBlitterFont font, Color color, float x, float y);
SizeF MeasureString(string s, IBlitterFont font);
Rectangle ClipBounds { get; set; }
}
public interface IBlitterFont { }
#if WINDOWS
public class Direct3DRenderPanel : IRenderer, IBlitter
{
public Color BackgroundColor { get; set; }
public bool Resized { get; set; }
public string FPS { get; set; }
public string MT { get; set; }
private readonly Direct3D d3d;
private Device _device;
private readonly Control backingControl;
public ImageTexture Texture;
private Sprite Sprite;
private d3d9font MessageFont;
private d3d9font AlertFont;
class FontWrapper : IBlitterFont
{
public FontWrapper(d3d9font font)
{
this.font = font;
}
public readonly d3d9font font;
}
void IBlitter.Open()
{
ClipBounds = new Rectangle(0, 0, NativeSize.Width, NativeSize.Height);
}
void IBlitter.Close() {}
private bool Vsync;
public Size NativeSize { get { return backingControl.ClientSize; } }
IBlitterFont IBlitter.GetFontType(string fontType)
{
if (fontType == "MessageFont") return new FontWrapper(MessageFont);
if (fontType == "AlertFont") return new FontWrapper(AlertFont);
return null;
}
Color4 col(Color c) { return new Color4(c.ToArgb()); }
void IBlitter.DrawString(string s, IBlitterFont font, Color color, float x, float y)
{
((FontWrapper)font).font.DrawString(null, s, (int)x + 1, (int)y + 1, col(color));
}
SizeF IBlitter.MeasureString(string s, IBlitterFont _font)
{
var font = ((FontWrapper)_font).font;
Rectangle r = font.MeasureString(null, s, DrawTextFormat.Left);
return new SizeF(r.Width, r.Height);
}
public Rectangle ClipBounds { get; set; }
public Direct3DRenderPanel(Direct3D direct3D, Control control)
{
d3d = direct3D;
backingControl = control;
control.MouseDoubleClick += (o, e) => HandleFullscreenToggle(o, e);
control.MouseClick += (o, e) => GlobalWin.MainForm.MainForm_MouseClick(o, e);
}
private void HandleFullscreenToggle(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
GlobalWin.MainForm.ToggleFullscreen();
}
private void DestroyDevice()
{
if (Texture != null)
{
Texture.Dispose();
Texture = null;
}
if (Sprite != null)
{
Sprite.Dispose();
Sprite = null;
}
if (_device != null)
{
_device.Dispose();
_device = null;
}
if (MessageFont != null)
{
MessageFont.Dispose();
MessageFont = null;
}
if (AlertFont != null)
{
AlertFont.Dispose();
AlertFont = null;
}
}
private bool VsyncRequested
{
get
{
if (Global.ForceNoThrottle)
return false;
return Global.Config.VSyncThrottle || Global.Config.VSync;
}
}
public void CreateDevice()
{
DestroyDevice();
Vsync = VsyncRequested;
var pp = new PresentParameters
{
BackBufferWidth = Math.Max(1, backingControl.ClientSize.Width),
BackBufferHeight = Math.Max(1, backingControl.ClientSize.Height),
DeviceWindowHandle = backingControl.Handle,
PresentationInterval = Vsync ? PresentInterval.One : PresentInterval.Immediate
};
var flags = CreateFlags.SoftwareVertexProcessing;
if ((d3d.GetDeviceCaps(0, DeviceType.Hardware).DeviceCaps & DeviceCaps.HWTransformAndLight) != 0)
{
flags = CreateFlags.HardwareVertexProcessing;
}
_device = new Device(d3d, 0, DeviceType.Hardware, backingControl.Handle, flags, pp);
Sprite = new Sprite(_device);
Texture = new ImageTexture(_device);
MessageFont = new d3d9font(_device, 16, 0, FontWeight.Bold, 1, false, CharacterSet.Default, Precision.Default, FontQuality.Default, PitchAndFamily.Default | PitchAndFamily.DontCare, "Courier");
AlertFont = new d3d9font(_device, 16, 0, FontWeight.ExtraBold, 1, true, CharacterSet.Default, Precision.Default, FontQuality.Default, PitchAndFamily.Default | PitchAndFamily.DontCare, "Courier");
// NOTE: if you add ANY objects, like new fonts, textures, etc, to this method
// ALSO add dispose code in DestroyDevice() or you will be responsible for VRAM memory leaks.
}
public void Render()
{
if (_device == null || Resized || Vsync != VsyncRequested)
backingControl.Invoke(() => CreateDevice());
Resized = false;
_device.Clear(ClearFlags.Target, BackgroundColor, 1.0f, 0);
Present();
}
public void FastRenderAndPresent(DisplaySurface surface)
{
Render(surface);
Present();
}
public void RenderOverlay(DisplaySurface surface)
{
RenderWrapper(() => RenderExec(surface, true));
}
public void Render(DisplaySurface surface)
{
RenderWrapper(() => RenderExec(surface, false));
}
public void RenderWrapper(Action todo)
{
try
{
todo();
}
catch (Direct3D9Exception)
{
// Wait until device is available or user gets annoyed and closes app
Result r;
// it can take a while for the device to be ready again, so avoid sound looping during the wait
if (GlobalWin.Sound != null) GlobalWin.Sound.StopSound();
do
{
r = _device.TestCooperativeLevel();
Thread.Sleep(100);
} while (r == ResultCode.DeviceLost);
if (GlobalWin.Sound != null) GlobalWin.Sound.StartSound();
// lets try recovery!
DestroyDevice();
backingControl.Invoke(() => CreateDevice());
todo();
}
}
void RenderPrep()
{
if (_device == null || Resized || Vsync != VsyncRequested)
backingControl.Invoke(() => CreateDevice());
Resized = false;
}
public void Clear(Color color)
{
_device.Clear(ClearFlags.Target, col(color), 0.0f, 0);
}
private void RenderExec(DisplaySurface surface, bool overlay)
{
RenderPrep();
if (surface == null)
{
Render();
return;
}
Texture.SetImage(surface, surface.Width, surface.Height);
if(!overlay) _device.Clear(ClearFlags.Target, BackgroundColor, 0.0f, 0);
// figure out scaling factor
float widthScale = (float)backingControl.Size.Width / surface.Width;
float heightScale = (float)backingControl.Size.Height / surface.Height;
float finalScale = Math.Min(widthScale, heightScale);
_device.BeginScene();
SpriteFlags flags = SpriteFlags.None;
if (overlay) flags |= SpriteFlags.AlphaBlend;
Sprite.Begin(flags);
if (Global.Config.DispBlurry)
{
_device.SetSamplerState(0, SamplerState.MagFilter, TextureFilter.Linear);
_device.SetSamplerState(0, SamplerState.MinFilter, TextureFilter.Linear);
}
else
{
_device.SetSamplerState(0, SamplerState.MagFilter, TextureFilter.Point);
_device.SetSamplerState(0, SamplerState.MinFilter, TextureFilter.Point);
}
Sprite.Transform = Matrix.Scaling(finalScale, finalScale, 0f);
Sprite.Draw(Texture.Texture, new Rectangle(0, 0, surface.Width, surface.Height), new Vector3(surface.Width / 2f, surface.Height / 2f, 0), new Vector3(backingControl.Size.Width / 2f / finalScale, backingControl.Size.Height / 2f / finalScale, 0), Color.White);
Sprite.End();
_device.EndScene();
}
public void Present()
{
// Present() is the most likely place to get DeviceLost, so we need to wrap it
RenderWrapper(_Present);
}
private readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
private readonly long stopwatchthrottle = System.Diagnostics.Stopwatch.Frequency / 50;
private long stopwatchtmr;
private void _Present()
{
// according to the internet, D3DPRESENT_DONOTWAIT is not terribly reliable
// so instead we measure the time the present takes, and drop the next present call if it was too long
// this code isn't really very good
if (Global.Config.VSync && !Global.Config.VSyncThrottle)
{
if (stopwatchtmr > stopwatchthrottle)
{
stopwatchtmr = 0;
//Console.WriteLine('s');
}
else
{
stopwatch.Restart();
//Device.GetSwapChain(0).Present(SlimDX.Direct3D9.Present.DoNotWait);
_device.Present(SlimDX.Direct3D9.Present.None);
stopwatch.Stop();
stopwatchtmr += stopwatch.ElapsedTicks;
//Console.WriteLine('.');
stopwatchtmr -= stopwatchthrottle / 4;
}
}
else
_device.Present(SlimDX.Direct3D9.Present.None);
}
// not used anywhere?
//public static EventWaitHandle vsyncEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
private bool disposed;
public void Dispose()
{
if (disposed == false)
{
disposed = true;
DestroyDevice();
}
}
public Point ScreenToScreen(Point p)
{
p = backingControl.PointToClient(p);
Point ret = new Point(p.X * Texture.ImageWidth / backingControl.Width,
p.Y * Texture.ImageHeight / backingControl.Height);
return ret;
}
}
#endif
class UIMessage
{
public string Message;
public DateTime ExpireAt;
}
class UIDisplay
{
public string Message;
public int X;
public int Y;
public bool Alert;
public int Anchor;
public Color ForeColor;
public Color BackGround;
}
}