using System; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Collections.Generic; using System.Windows.Forms; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; //using dx=SlimDX; //using d3d=SlimDX.Direct3D9; namespace BizHawk.MultiClient { /// /// encapsulates thread-safe concept of pending/current display surfaces, reusing buffers where matching /// sizes are available and keeping them cleaned up when they dont seem like theyll need to be used anymore /// class SwappableDisplaySurfaceSet { DisplaySurface Pending, Current; Queue ReleasedSurfaces = new Queue(); /// /// retrieves a surface with the specified size, reusing an old buffer if available and clearing if requested /// public DisplaySurface AllocateSurface(int width, int height, bool needsClear = true) { for (; ; ) { DisplaySurface 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 new DisplaySurface(width, height); } /// /// sets the provided buffer as pending. takes control of the supplied buffer /// public void SetPending(DisplaySurface newPending) { lock (this) { if (Pending != null) ReleasedSurfaces.Enqueue(Pending); Pending = newPending; } } public void ReleaseSurface(DisplaySurface surface) { lock (this) ReleasedSurfaces.Enqueue(surface); } /// /// returns the current buffer, making the most recent pending buffer (if there is such) as the new current first. /// public DisplaySurface GetCurrent() { lock (this) { if (Pending != null) { if (Current != null) ReleasedSurfaces.Enqueue(Current); Current = Pending; Pending = null; } } return Current; } } public interface IDisplayFilter { /// /// describes how this filter will respond to an input format /// DisplayFilterAnalysisReport Analyze(Size sourceSize); /// /// runs the filter /// DisplaySurface Execute(DisplaySurface surface); } public class DisplayFilterAnalysisReport { public bool Success; public Size OutputSize; } interface IDisplayDriver { } class Direct3DDisplayDriver : IDisplayDriver { } public unsafe class DisplaySurface : IDisposable { Bitmap bmp; BitmapData bmpdata; int[] pixels; public unsafe void Clear() { FromBitmap(false); Util.memset(PixelPtr, 0, Stride * Height); } public Bitmap PeekBitmap() { ToBitmap(); return bmp; } /// /// returns a Graphics object used to render to this surface. be sure to dispose it! /// public Graphics GetGraphics() { ToBitmap(); return Graphics.FromImage(bmp); } public unsafe void ToBitmap(bool copy=true) { if (isBitmap) return; isBitmap = true; if (bmp == null) { bmp = new Bitmap(Width, Height, PixelFormat.Format32bppArgb); } if (copy) { bmpdata = bmp.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); int w = Width; int h = Height; int stride = bmpdata.Stride / 4; int* bmpbuf = (int*)bmpdata.Scan0.ToPointer(); for (int y = 0, i = 0; y < h; y++) for (int x = 0; x < w; x++) bmpbuf[y * stride + x] = pixels[i++]; bmp.UnlockBits(bmpdata); } } public bool IsBitmap { get { return isBitmap; } } bool isBitmap = false; public unsafe void FromBitmap(bool copy=true) { if (!isBitmap) return; isBitmap = false; if (copy) { bmpdata = bmp.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); int w = Width; int h = Height; int stride = bmpdata.Stride / 4; int* bmpbuf = (int*)bmpdata.Scan0.ToPointer(); for (int y = 0, i = 0; y < h; y++) for (int x = 0; x < w; x++) pixels[i++] = bmpbuf[y * stride + x]; bmp.UnlockBits(bmpdata); } } public static DisplaySurface DisplaySurfaceWrappingBitmap(Bitmap bmp) { DisplaySurface ret = new DisplaySurface(); ret.Width = bmp.Width; ret.Height = bmp.Height; ret.bmp = bmp; ret.isBitmap = true; return ret; } private DisplaySurface() { } 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; pixels = new int[width * height]; LockPixels(); } public int* PixelPtr { get { return (int*)ptr; } } public IntPtr PixelIntPtr { get { return new IntPtr(ptr); } } public int Stride { get { return Width*4; } } public int OffsetOf(int x, int y) { return y * Stride + x*4; } void* ptr; GCHandle handle; void LockPixels() { UnlockPixels(); handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); ptr = handle.AddrOfPinnedObject().ToPointer(); } void UnlockPixels() { if(handle.IsAllocated) handle.Free(); } /// /// returns a new surface /// /// /// /// public DisplaySurface ToPaddedSurface(int xpad0, int ypad0, int xpad1, int ypad1) { int new_width = Width + xpad0 + xpad1; int new_height = Height + ypad0 + ypad1; DisplaySurface ret = new DisplaySurface(new_width, new_height); int* dptr = ret.PixelPtr; int* sptr = PixelPtr; int dstride = ret.Stride / 4; int sstride = Stride / 4; for (int y = 0; y < Height; y++) for (int x = 0; x < Width; x++) { dptr[(y + ypad0) * dstride + x + xpad0] = sptr[y * sstride + x]; } return ret; } public int Width { get; private set; } public int Height { get; private set; } public void Dispose() { if (bmp != null) bmp.Dispose(); bmp = null; UnlockPixels(); } //public unsafe int[] ToIntArray() { } public void AcceptIntArray(int[] newpixels) { FromBitmap(false); UnlockPixels(); pixels = newpixels; LockPixels(); } } public class OSDManager { public string FPS { get; set; } public string MT { get; set; } IBlitterFont MessageFont; IBlitterFont AlertFont; public void Dispose() { } public void Begin(IBlitter blitter) { MessageFont = blitter.GetFontType("MessageFont"); AlertFont = blitter.GetFontType("AlertFont"); } public System.Drawing.Color FixedMessagesColor { get { return System.Drawing.Color.FromArgb(Global.Config.MessagesColor); } } public System.Drawing.Color FixedAlertMessageColor { get { return System.Drawing.Color.FromArgb(Global.Config.AlertMessageColor); } } public OSDManager() { } private float GetX(IBlitter g, int x, int anchor, IBlitterFont font, string message) { var size = g.MeasureString(message, font); //Rectangle rect = g.MeasureString(Sprite, message, new DrawTextFormat()); switch (anchor) { default: case 0: //Top Left case 2: //Bottom Left return x; case 1: //Top Right case 3: //Bottom Right return g.ClipBounds.Width - x - size.Width; } } private float GetY(IBlitter g, int y, int anchor, IBlitterFont font, string message) { var size = g.MeasureString(message, font); switch (anchor) { default: case 0: //Top Left case 1: //Top Right return y; case 2: //Bottom Left case 3: //Bottom Right return g.ClipBounds.Height - y - size.Height; } } private string MakeFrameCounter() { if (Global.MovieSession.Movie.IsFinished) { return Global.Emulator.Frame.ToString() + "/" + Global.MovieSession.Movie.Frames.ToString() + " (Finished)"; } else if (Global.MovieSession.Movie.IsPlaying) { return Global.Emulator.Frame.ToString() + "/" + Global.MovieSession.Movie.Frames.ToString(); } else if (Global.MovieSession.Movie.IsRecording) { return Global.Emulator.Frame.ToString(); } else { return Global.Emulator.Frame.ToString(); } } private string MakeLagCounter() { return Global.Emulator.LagCount.ToString(); } private List messages = new List(5); private List GUITextList = new List(); public void AddMessage(string message) { messages.Add(new UIMessage { Message = message, ExpireAt = DateTime.Now + TimeSpan.FromSeconds(2) }); } public void AddGUIText(string message, int x, int y, bool alert, Color BackGround, Color ForeColor, int anchor) { GUITextList.Add(new UIDisplay { Message = message, X = x, Y = y, BackGround = BackGround, ForeColor = ForeColor, Alert = alert, Anchor = anchor }); } public void ClearGUIText() { GUITextList.Clear(); } public void DrawMessages(IBlitter g) { if (!Global.ClientControls["MaxTurbo"]) { messages.RemoveAll(m => DateTime.Now > m.ExpireAt); int line = 1; if (Global.Config.StackOSDMessages) { for (int i = messages.Count - 1; i >= 0; i--, line++) { float x = GetX(g, Global.Config.DispMessagex, Global.Config.DispMessageanchor, MessageFont, messages[i].Message); float y = GetY(g, Global.Config.DispMessagey, Global.Config.DispMessageanchor, MessageFont, messages[i].Message); if (Global.Config.DispMessageanchor < 2) { y += ((line - 1) * 18); } else { y -= ((line - 1) * 18); } g.DrawString(messages[i].Message, MessageFont, Color.Black, x + 2, y + 2); g.DrawString(messages[i].Message, MessageFont, FixedMessagesColor, x, y); } } else { if (messages.Count > 0) { int i = messages.Count - 1; float x = GetX(g, Global.Config.DispMessagex, Global.Config.DispMessageanchor, MessageFont, messages[i].Message); float y = GetY(g, Global.Config.DispMessagey, Global.Config.DispMessageanchor, MessageFont, messages[i].Message); if (Global.Config.DispMessageanchor < 2) { y += ((line - 1) * 18); } else { y -= ((line - 1) * 18); } g.DrawString(messages[i].Message, MessageFont, Color.Black, x + 2, y + 2); g.DrawString(messages[i].Message, MessageFont, FixedMessagesColor, x, y); } } for (int x = 0; x < GUITextList.Count; x++) { try { float posx = GetX(g, GUITextList[x].X, GUITextList[x].Anchor, MessageFont, GUITextList[x].Message); float posy = GetY(g, GUITextList[x].Y, GUITextList[x].Anchor, MessageFont, GUITextList[x].Message); g.DrawString(GUITextList[x].Message, MessageFont, GUITextList[x].BackGround, posx + 2, posy + 2); //g.DrawString(GUITextList[x].Message, MessageFont, Color.Gray, posx + 1, posy + 1); if (GUITextList[x].Alert) g.DrawString(GUITextList[x].Message, MessageFont, FixedMessagesColor, posx, posy); else g.DrawString(GUITextList[x].Message, MessageFont, GUITextList[x].ForeColor, posx, posy); } catch (Exception) { return; } } } } public string MakeInputDisplay() { StringBuilder s; if (!Global.MovieSession.Movie.IsActive || Global.MovieSession.Movie.IsFinished) { s = new StringBuilder(Global.GetOutputControllersAsMnemonic()); } else { s = new StringBuilder(Global.MovieSession.Movie.GetInput(Global.Emulator.Frame - 1)); } s.Replace(".", " ").Replace("|", "").Replace("l", ""); //Note: if l is ever a mnemonic this will squash it. But this is done because on the NES core I put l in the command mnemonic to indicate lag (was a bad a idea) return s.ToString(); } public string MakeRerecordCount() { if (Global.MovieSession.Movie.IsActive) { return "Rerecord Count: " + Global.MovieSession.Movie.Rerecords.ToString(); } else { return ""; } } /// /// Display all screen info objects like fps, frame counter, lag counter, and input display /// public void DrawScreenInfo(IBlitter g) { if (Global.Config.DisplayFrameCounter) { string message = MakeFrameCounter(); float x = GetX(g, Global.Config.DispFrameCx, Global.Config.DispFrameanchor, MessageFont, message); float y = GetY(g, Global.Config.DispFrameCy, Global.Config.DispFrameanchor, MessageFont, message); g.DrawString(message, MessageFont, Color.Black, x + 1, y + 1); g.DrawString(message, MessageFont, Color.FromArgb(Global.Config.MessagesColor), x, y); } if (Global.Config.DisplayInput) { string input = MakeInputDisplay(); Color c; float x = GetX(g, Global.Config.DispInpx, Global.Config.DispInpanchor, MessageFont, input); float y = GetY(g, Global.Config.DispInpy, Global.Config.DispInpanchor, MessageFont, input); if (Global.MovieSession.Movie.IsPlaying && !Global.MovieSession.Movie.IsRecording) { c = Color.FromArgb(Global.Config.MovieInput); } else { c = Color.FromArgb(Global.Config.MessagesColor); } g.DrawString(input, MessageFont, Color.Black, x+1,y+1); g.DrawString(input, MessageFont, c, x,y); } if (Global.MovieSession.MultiTrack.IsActive) { g.DrawString(MT, MessageFont, Color.Black, Global.Config.DispFPSx + 1, //TODO: Multitrack position variables Global.Config.DispFPSy + 1); g.DrawString(MT, MessageFont, FixedMessagesColor, Global.Config.DispFPSx, //TODO: Multitrack position variables Global.Config.DispFPSy); } if (Global.Config.DisplayFPS && FPS != null) { float x = GetX(g, Global.Config.DispFPSx, Global.Config.DispFPSanchor, MessageFont, FPS); float y = GetY(g, Global.Config.DispFPSy, Global.Config.DispFPSanchor, MessageFont, FPS); g.DrawString(FPS, MessageFont, Color.Black, x + 1, y + 1); g.DrawString(FPS, MessageFont, FixedMessagesColor, x, y); } if (Global.Config.DisplayLagCounter) { string counter = MakeLagCounter(); if (Global.Emulator.IsLagFrame) { float x = GetX(g, Global.Config.DispLagx, Global.Config.DispLaganchor, AlertFont, counter); float y = GetY(g, Global.Config.DispLagy, Global.Config.DispLaganchor, AlertFont, counter); g.DrawString(counter, AlertFont, Color.Black, x + 1, y + 1); g.DrawString(counter, AlertFont, FixedAlertMessageColor, x, y); } else { float x = GetX(g, Global.Config.DispLagx, Global.Config.DispLaganchor, MessageFont, counter); float y = GetY(g, Global.Config.DispLagy, Global.Config.DispLaganchor, MessageFont, counter); g.DrawString(counter, MessageFont, Color.Black, x + 1, y + 1); g.DrawString(counter, MessageFont, FixedMessagesColor, x , y ); } } if (Global.Config.DisplayRerecordCount) { string rerec = MakeRerecordCount(); float x = GetX(g, Global.Config.DispRecx, Global.Config.DispRecanchor, MessageFont, rerec); float y = GetY(g, Global.Config.DispRecy, Global.Config.DispRecanchor, MessageFont, rerec); g.DrawString(rerec, MessageFont, Color.Black, x + 1, y + 1); g.DrawString(rerec, MessageFont, FixedMessagesColor, x, y); } if (Global.ClientControls["Autohold"]) { StringBuilder disp = new StringBuilder("Held: "); foreach (string s in Global.StickyXORAdapter.CurrentStickies) { disp.Append(s); disp.Append(' '); } foreach (string s in Global.AutofireStickyXORAdapter.CurrentStickies) { disp.Append("Auto-"); disp.Append(s); disp.Append(' '); } g.DrawString(disp.ToString(), MessageFont, Color.White, GetX(g, Global.Config.DispAutoholdx, Global.Config.DispAutoholdanchor, MessageFont, disp.ToString()), GetY(g, Global.Config.DispAutoholdy, Global.Config.DispAutoholdanchor, MessageFont, disp.ToString())); } //TODO //if (Global.MovieSession.Movie.IsPlaying) //{ // //int r = (int)g.ClipBounds.Width; // //Point[] p = { new Point(r - 20, 2), // // new Point(r - 4, 12), // // new Point(r - 20, 22) }; // //g.FillPolygon(new SolidBrush(Color.Red), p); // //g.DrawPolygon(new Pen(new SolidBrush(Color.Pink)), p); //} //else if (Global.MovieSession.Movie.IsRecording) //{ // //g.FillEllipse(new SolidBrush(Color.Red), new Rectangle((int)g.ClipBounds.Width - 22, 2, 20, 20)); // //g.DrawEllipse(new Pen(new SolidBrush(Color.Pink)), new Rectangle((int)g.ClipBounds.Width - 22, 2, 20, 20)); //} if (Global.MovieSession.Movie.IsActive && Global.Config.DisplaySubtitles) { List s = Global.MovieSession.Movie.Subtitles.GetSubtitles(Global.Emulator.Frame); if (s == null) { return; } for (int i = 0; i < s.Count; i++) { g.DrawString(s[i].Message, MessageFont, Color.Black, s[i].X + 1, s[i].Y + 1); g.DrawString(s[i].Message, MessageFont, Color.FromArgb((int)s[i].Color), s[i].X, s[i].Y); } } } } public class DisplayManager : IDisposable { public DisplayManager() { //have at least something here at the start luaNativeSurfacePreOSD = new DisplaySurface(1, 1); } volatile bool VsyncToggle = false; volatile bool VsyncRequested = false; SwappableDisplaySurfaceSet sourceSurfaceSet = new SwappableDisplaySurfaceSet(); DisplaySurface luaEmuSurface = null; public void PreFrameUpdateLuaSource() { luaEmuSurface = luaEmuSurfaceSet.GetCurrent(); } /// update Global.RenderPanel from the passed IVideoProvider public void UpdateSource(IVideoProvider videoProvider) { UpdateSourceEx(videoProvider, Global.RenderPanel); } /// /// update the passed IRenderer with the passed IVideoProvider /// /// /// also must implement IBlitter public void UpdateSourceEx(IVideoProvider videoProvider, IRenderer renderPanel) { var newPendingSurface = sourceSurfaceSet.AllocateSurface(videoProvider.BufferWidth, videoProvider.BufferHeight, false); newPendingSurface.AcceptIntArray(videoProvider.GetVideoBuffer()); sourceSurfaceSet.SetPending(newPendingSurface); if (renderPanel == null) return; currNativeWidth = renderPanel.NativeSize.Width; currNativeHeight = renderPanel.NativeSize.Height; currentSourceSurface = sourceSurfaceSet.GetCurrent(); if (currentSourceSurface == null) return; //if we're configured to use a scaling filter, apply it now //SHOULD THIS BE RUN REPEATEDLY? //some filters may need to run repeatedly (temporal interpolation, ntsc scanline field alternating) //but its sort of wasted work. CheckFilter(); int w = currNativeWidth; int h = currNativeHeight; DisplaySurface luaSurface = luaNativeSurfaceSet.GetCurrent(); //do we have anything to do? //bool complexComposite = false; //if (luaEmuSurface != null) complexComposite = true; //if (luaSurface != null) complexComposite = true; DisplaySurface surfaceToRender = filteredSurface; if (surfaceToRender == null) surfaceToRender = currentSourceSurface; renderPanel.Clear(Color.FromArgb(videoProvider.BackgroundColor)); renderPanel.Render(surfaceToRender); if (luaEmuSurface != null) { renderPanel.RenderOverlay(luaEmuSurface); } RenderOSD((IBlitter)renderPanel); renderPanel.Present(); if (filteredSurface != null) filteredSurface.Dispose(); filteredSurface = null; } public bool Disposed { get; private set; } public void Dispose() { if (Disposed) return; Disposed = true; } DisplaySurface currentSourceSurface, filteredSurface; //the surface to use to render a lua layer at native resolution (under the OSD) DisplaySurface luaNativeSurfacePreOSD; SwappableDisplaySurfaceSet luaNativeSurfaceSet = new SwappableDisplaySurfaceSet(); public void SetLuaSurfaceNativePreOSD(DisplaySurface surface) { luaNativeSurfaceSet.SetPending(surface); } public DisplaySurface GetLuaSurfaceNative() { return luaNativeSurfaceSet.AllocateSurface(currNativeWidth, currNativeHeight); } SwappableDisplaySurfaceSet luaEmuSurfaceSet = new SwappableDisplaySurfaceSet(); public void SetLuaSurfaceEmu(DisplaySurface surface) { luaEmuSurfaceSet.SetPending(surface); } public DisplaySurface GetLuaEmuSurfaceEmu() { int width = 1, height = 1; if (currentSourceSurface != null) width = currentSourceSurface.Width; if (currentSourceSurface != null) height = currentSourceSurface.Height; return luaEmuSurfaceSet.AllocateSurface(width, height); } int currNativeWidth, currNativeHeight; /// /// suspends the display manager so that tricky things can be changed without the display thread going in and getting all confused and hating /// public void Suspend() { } /// /// resumes the display manager after a suspend /// public void Resume() { } void RenderOSD(IBlitter renderPanel) { Global.OSD.Begin(renderPanel); renderPanel.Open(); Global.OSD.DrawScreenInfo(renderPanel); Global.OSD.DrawMessages(renderPanel); renderPanel.Close(); } void CheckFilter() { IDisplayFilter filter = null; switch (Global.Config.TargetDisplayFilter) { case 0: //no filter break; case 1: filter = new Hq2xBase_2xSai(); break; case 2: filter = new Hq2xBase_Super2xSai(); break; case 3: filter = new Hq2xBase_SuperEagle(); break; } if (filter == null) filteredSurface = null; else filteredSurface = filter.Execute(currentSourceSurface); } SwappableDisplaySurfaceSet nativeDisplaySurfaceSet = new SwappableDisplaySurfaceSet(); Thread displayThread; } }