BizHawk/src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs

1096 lines
37 KiB
C#

// TODO
// we could flag textures as 'actually' render targets (keep a reference to the render target?) which could allow us to convert between them more quickly in some cases
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Text;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using BizHawk.Bizware.BizwareGL;
using BizHawk.Bizware.BizwareGL.DrawingExtensions;
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;
using BizHawk.Emulation.Cores.Consoles.Nintendo.NDS;
using BizHawk.Emulation.Cores.Sony.PSX;
namespace BizHawk.Client.Common
{
/// <summary>
/// A DisplayManager is destined forevermore to drive the PresentationPanel it gets initialized with.
/// Its job is to receive OSD and emulator outputs, and produce one single buffer (BitmapBuffer? Texture2d?) for display by the PresentationPanel.
/// Details TBD
/// </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, RenderTarget> _callback;
RenderTarget IRenderTargetProvider.Get(Size size)
{
return _callback(size);
}
public DisplayManagerRenderTargetProvider(Func<Size, RenderTarget> callback)
{
_callback = callback;
}
}
public OSDManager OSD { get; }
protected Config GlobalConfig;
private IEmulator GlobalEmulator;
protected DisplayManagerBase(
Config config,
IEmulator emulator,
InputManager inputManager,
IMovieSession movieSession,
EDispMethod dispMethod,
IGL gl,
IGuiRenderer renderer)
{
GlobalConfig = config;
GlobalEmulator = emulator;
OSD = new(config, emulator, inputManager, movieSession);
_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
for (var i = 0; i < 16; i++)
{
_shaderChainFrugalizers[i] = new(_gl);
}
{
using var xml = ReflectionCache.EmbeddedResourceStream("Resources.courier16px.fnt");
using var tex = ReflectionCache.EmbeddedResourceStream("Resources.courier16px_0.png");
_theOneFont = new(_gl, xml, tex);
using var gens = ReflectionCache.EmbeddedResourceStream("Resources.gens.ttf");
LoadCustomFont(gens);
using var fceux = ReflectionCache.EmbeddedResourceStream("Resources.fceux.ttf");
LoadCustomFont(fceux);
}
if (dispMethod is EDispMethod.OpenGL or EDispMethod.D3D9)
{
var fiHq2x = new FileInfo(Path.Combine(PathUtils.ExeDirectoryPath, "Shaders/BizHawk/hq2x.cgp"));
if (fiHq2x.Exists)
{
using var stream = fiHq2x.OpenRead();
_shaderChainHq2X = new(_gl, new(stream), Path.Combine(PathUtils.ExeDirectoryPath, "Shaders/BizHawk"));
}
var fiScanlines = new FileInfo(Path.Combine(PathUtils.ExeDirectoryPath, "Shaders/BizHawk/BizScanlines.cgp"));
if (fiScanlines.Exists)
{
using var stream = fiScanlines.OpenRead();
_shaderChainScanlines = new(_gl, new(stream), Path.Combine(PathUtils.ExeDirectoryPath, "Shaders/BizHawk"));
}
var bicubicPath = dispMethod == EDispMethod.D3D9 ? "Shaders/BizHawk/bicubic-normal.cgp" : "Shaders/BizHawk/bicubic-fast.cgp";
var fiBicubic = new FileInfo(Path.Combine(PathUtils.ExeDirectoryPath, bicubicPath));
if (fiBicubic.Exists)
{
using var stream = fiBicubic.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
_shaderChainBicubic = new(_gl, new(stream), Path.Combine(PathUtils.ExeDirectoryPath, "Shaders/BizHawk"));
}
}
_apiHawkSurfaceSets[DisplaySurfaceID.EmuCore] = new(CreateDisplaySurface);
_apiHawkSurfaceSets[DisplaySurfaceID.Client] = new(CreateDisplaySurface);
_apiHawkSurfaceFrugalizers[DisplaySurfaceID.EmuCore] = new(_gl);
_apiHawkSurfaceFrugalizers[DisplaySurfaceID.Client] = new(_gl);
RefreshUserShader();
}
public void UpdateGlobals(Config config, IEmulator emulator)
{
GlobalConfig = config;
GlobalEmulator = emulator;
OSD.UpdateGlobals(config, emulator);
}
public bool Disposed { get; private set; }
public void Dispose()
{
if (Disposed)
{
return;
}
Disposed = true;
// OpenGL context needs to be active when Dispose()'ing
ActivateOpenGLContext();
_videoTextureFrugalizer.Dispose();
foreach (var f in _apiHawkSurfaceFrugalizers.Values)
{
f.Dispose();
}
foreach (var f in _shaderChainFrugalizers)
{
f?.Dispose();
}
foreach (var s in new[] { _shaderChainHq2X, _shaderChainScanlines, _shaderChainBicubic, _shaderChainUser })
{
s?.Dispose();
}
_theOneFont.Dispose();
_renderer.Dispose();
}
// rendering resources:
protected readonly IGL _gl;
private readonly StringRenderer _theOneFont;
private readonly IGuiRenderer _renderer;
// 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>
public (int Left, int Top, int Right, int Bottom) GameExtraPadding { get; set; }
/// <summary>
/// additional pixels added at the native level for the use of lua drawing. essentially just gets tacked onto the final calculated window sizes.
/// </summary>
public (int Left, int Top, int Right, int Bottom) ClientExtraPadding { get; set; }
/// <summary>
/// custom fonts that don't need to be installed on the user side
/// </summary>
public PrivateFontCollection CustomFonts { get; } = new();
private readonly TextureFrugalizer _videoTextureFrugalizer;
private readonly Dictionary<DisplaySurfaceID, TextureFrugalizer> _apiHawkSurfaceFrugalizers = new();
protected readonly RenderTargetFrugalizer[] _shaderChainFrugalizers;
private readonly RetroShaderChain _shaderChainHq2X;
private readonly RetroShaderChain _shaderChainScanlines;
private readonly RetroShaderChain _shaderChainBicubic;
private RetroShaderChain _shaderChainUser;
public abstract void ActivateOpenGLContext();
protected abstract void ActivateGraphicsControlContext();
protected abstract void SwapBuffersOfGraphicsControl();
public void RefreshUserShader()
{
_shaderChainUser?.Dispose();
if (File.Exists(GlobalConfig.DispUserFilterPath))
{
var fi = new FileInfo(GlobalConfig.DispUserFilterPath);
using var stream = fi.OpenRead();
_shaderChainUser = new(_gl, new(stream), Path.GetDirectoryName(GlobalConfig.DispUserFilterPath));
}
}
private (int Left, int Top, int Right, int Bottom) CalculateCompleteContentPadding(bool user, bool source)
{
var padding = (Left: 0, Top: 0, Right: 0, Bottom: 0);
if (user)
{
padding.Left += GameExtraPadding.Left;
padding.Top += GameExtraPadding.Top;
padding.Right += GameExtraPadding.Right;
padding.Bottom += GameExtraPadding.Bottom;
}
// an experimental feature
if (source && GlobalEmulator is Octoshock psx)
{
var corePadding = psx.VideoProvider_Padding;
padding.Left += corePadding.Width / 2;
padding.Right += corePadding.Width - corePadding.Width / 2;
padding.Top += corePadding.Height / 2;
padding.Bottom += corePadding.Height - corePadding.Height / 2;
}
// apply user's crop selections as a negative padding (believe it or not, this largely works)
// is there an issue with the aspect ratio? I don't know--but if there is, there would be with the padding too
padding.Left -= GlobalConfig.DispCropLeft;
padding.Right -= GlobalConfig.DispCropRight;
padding.Top -= GlobalConfig.DispCropTop;
padding.Bottom -= GlobalConfig.DispCropBottom;
return padding;
}
private (int Horizontal, int Vertical) CalculateCompleteContentPaddingSum(bool user, bool source)
{
var p = CalculateCompleteContentPadding(user: user, source: source);
return (p.Left + p.Right, p.Top + p.Bottom);
}
private FilterProgram BuildDefaultChain(Size chainInSize, Size chainOutSize, bool includeOSD, bool includeUserFilters)
{
// select user special FX shader chain
var selectedChainProperties = new Dictionary<string, object>();
RetroShaderChain selectedChain = null;
switch (GlobalConfig.TargetDisplayFilter)
{
case 1 when _shaderChainHq2X is { Available: true }:
selectedChain = _shaderChainHq2X;
break;
case 2 when _shaderChainScanlines is { Available: true }:
selectedChain = _shaderChainScanlines;
selectedChainProperties["uIntensity"] = 1.0f - GlobalConfig.TargetScanlineFilterIntensity / 256.0f;
break;
case 3 when _shaderChainUser is { Available: true }:
selectedChain = _shaderChainUser;
break;
}
if (!includeUserFilters)
selectedChain = null;
var fCoreScreenControl = CreateCoreScreenControl();
var fPresent = new FinalPresentation(chainOutSize);
var fInput = new SourceImage(chainInSize);
var fOSD = new OSD();
fOSD.RenderCallback = () =>
{
if (!includeOSD)
{
return;
}
var size = fOSD.FindInput().SurfaceFormat.Size;
_renderer.Begin(size.Width, size.Height);
var myBlitter = new MyBlitter(this)
{
ClipBounds = new Rectangle(0, 0, size.Width, size.Height)
};
_renderer.EnableBlending();
OSD.Begin(myBlitter);
OSD.DrawScreenInfo(myBlitter);
OSD.DrawMessages(myBlitter);
_renderer.End();
};
var chain = new FilterProgram();
//add the first filter, encompassing output from the emulator core
chain.AddFilter(fInput, "input");
if (fCoreScreenControl != null)
{
chain.AddFilter(fCoreScreenControl, "CoreScreenControl");
}
// if a non-zero padding is required, add a filter to allow for that
// note, we have two sources of padding right now.. one can come from the VideoProvider and one from the user.
// we're combining these now and just using black, for sake of being lean, despite the discussion below:
// keep in mind, the VideoProvider design in principle might call for another color.
// we haven't really been using this very hard, but users will probably want black there (they could fill it to another color if needed tho)
var padding = CalculateCompleteContentPadding(true, true);
if (padding != (0, 0, 0, 0))
{
// TODO - add another filter just for this, its cumbersome to use final presentation... I think. but maybe there's enough similarities to justify it.
var size = chainInSize;
size.Width += padding.Left + padding.Right;
size.Height += padding.Top + padding.Bottom;
// in case the user requested so much padding that the dimensions are now negative, just turn it to something small
if (size.Width < 1) size.Width = 1;
if (size.Height < 1) size.Height = 1;
var fPadding = new FinalPresentation(size);
chain.AddFilter(fPadding, "padding");
fPadding.Config_PadOnly = true;
fPadding.Padding = padding;
}
// add lua layer 'emu'
AppendApiHawkLayer(chain, DisplaySurfaceID.EmuCore);
if (includeUserFilters)
{
if (GlobalConfig.DispPrescale != 1)
{
var fPrescale = new PrescaleFilter() { Scale = GlobalConfig.DispPrescale };
chain.AddFilter(fPrescale, "user_prescale");
}
}
// add user-selected retro shader
if (selectedChain != null)
{
AppendRetroShaderChain(chain, "retroShader", selectedChain, selectedChainProperties);
}
// AutoPrescale makes no sense for a None final filter
if (GlobalConfig.DispAutoPrescale && GlobalConfig.DispFinalFilter != (int)FinalPresentation.eFilterOption.None)
{
var apf = new AutoPrescaleFilter();
chain.AddFilter(apf, "auto_prescale");
}
// choose final filter
var finalFilter = GlobalConfig.DispFinalFilter switch
{
1 => FinalPresentation.eFilterOption.Bilinear,
2 => FinalPresentation.eFilterOption.Bicubic,
_ => FinalPresentation.eFilterOption.None
};
// if bicubic is selected and unavailable, don't use it. use bilinear instead I guess
if (finalFilter == FinalPresentation.eFilterOption.Bicubic)
{
if (_shaderChainBicubic is not { Available: true })
{
finalFilter = FinalPresentation.eFilterOption.Bilinear;
}
}
fPresent.FilterOption = finalFilter;
// now if bicubic is chosen, insert it
if (finalFilter == FinalPresentation.eFilterOption.Bicubic)
{
AppendRetroShaderChain(chain, "bicubic", _shaderChainBicubic, null);
}
// add final presentation
if (includeUserFilters)
chain.AddFilter(fPresent, "presentation");
//add lua layer 'native'
AppendApiHawkLayer(chain, DisplaySurfaceID.Client);
// and OSD goes on top of that
// TODO - things break if this isn't present (the final presentation filter gets messed up when used with prescaling)
// so, always include it (we'll handle this flag in the callback to do no rendering)
if (true /*includeOSD*/)
{
chain.AddFilter(fOSD, "osd");
}
return chain;
}
private static void AppendRetroShaderChain(FilterProgram program, string name, RetroShaderChain retroChain, Dictionary<string, object> properties)
{
for (var i = 0; i < retroChain.Passes.Length; i++)
{
var rsp = new RetroShaderPass(retroChain, i);
var fname = $"{name}[{i}]";
program.AddFilter(rsp, fname);
rsp.Parameters = properties;
}
}
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());
}
protected abstract Point GraphicsControlPointToClient(Point p);
/// <summary>
/// Using the current filter program, turn a mouse coordinate from window space to the original emulator screen space.
/// </summary>
public Point UntransformPoint(Point p)
{
// first, turn it into a window coordinate
p = GraphicsControlPointToClient(p);
// now, if there's no filter program active, just give up
if (_currentFilterProgram == null) return p;
// otherwise, have the filter program untransform it
var v = new Vector2(p.X, p.Y);
v = _currentFilterProgram.UntransformPoint("default", v);
return new((int)v.X, (int)v.Y);
}
/// <summary>
/// Using the current filter program, turn a emulator screen space coordinate to a window coordinate (suitable for lua layer drawing)
/// </summary>
public Point TransformPoint(Point p)
{
// now, if there's no filter program active, just give up
if (_currentFilterProgram == null)
{
return p;
}
// otherwise, have the filter program untransform it
var v = new Vector2(p.X, p.Y);
v = _currentFilterProgram.TransformPoint("default", v);
return new((int)v.X, (int)v.Y);
}
public abstract Size GetPanelNativeSize();
protected abstract Size GetGraphicsControlSize();
/// <summary>
/// This will receive an emulated output frame from an IVideoProvider and run it through the complete frame processing pipeline
/// Then it will stuff it into the bound PresentationPanel.
/// </summary>
public void UpdateSource(IVideoProvider videoProvider)
{
var displayNothing = GlobalConfig.DispSpeedupFeatures == 0;
var job = new JobInfo
{
VideoProvider = videoProvider,
Simulate = displayNothing,
ChainOutsize = GetGraphicsControlSize(),
IncludeOSD = true,
IncludeUserFilters = true
};
UpdateSourceInternal(job);
}
private BaseFilter CreateCoreScreenControl()
{
return GlobalEmulator switch
{
NDS nds => new ScreenControlNDS(nds),
Citra citra => new ScreenControl3DS(citra),
_ => null
};
}
/// <summary>
/// Does the entire display process to an offscreen buffer, suitable for a 'client' screenshot.
/// </summary>
public BitmapBuffer RenderOffscreen(IVideoProvider videoProvider, bool includeOSD)
{
var job = new JobInfo
{
VideoProvider = videoProvider,
Simulate = false,
ChainOutsize = GetGraphicsControlSize(),
Offscreen = true,
IncludeOSD = includeOSD,
IncludeUserFilters = true,
};
UpdateSourceInternal(job);
return job.OffscreenBb;
}
/// <summary>
/// Does the display process to an offscreen buffer, suitable for a Lua-inclusive movie.
/// </summary>
public BitmapBuffer RenderOffscreenLua(IVideoProvider videoProvider)
{
var job = new JobInfo
{
VideoProvider = videoProvider,
Simulate = false,
ChainOutsize = new(videoProvider.BufferWidth, videoProvider.BufferHeight),
Offscreen = true,
IncludeOSD = false,
IncludeUserFilters = false,
};
UpdateSourceInternal(job);
return job.OffscreenBb;
}
private class FakeVideoProvider : IVideoProvider
{
public FakeVideoProvider(int bw, int bh, int vw, int vh)
{
BufferWidth = bw;
BufferHeight = bh;
VirtualWidth = vw;
VirtualHeight = vh;
}
public int[] GetVideoBuffer()
=> Array.Empty<int>();
public int VirtualWidth { get; }
public int VirtualHeight { get; }
public int BufferWidth { get; }
public int BufferHeight { get; }
public int BackgroundColor => 0;
public int VsyncNumerator => throw new NotImplementedException();
public int VsyncDenominator => throw new NotImplementedException();
}
private static void FixRatio(float x, float y, int inw, int inh, out int outW, out int outH)
{
var ratio = x / y;
if (ratio <= 1)
{
// taller. weird. expand height.
outW = inw;
outH = (int)(inw / ratio);
}
else
{
// wider. normal. expand width.
outW = (int)(inh * ratio);
outH = inh;
}
}
/// <summary>
/// Attempts to calculate a good client size with the given zoom factor, considering the user's DisplayManager preferences
/// TODO - this needs to be redone with a concept different from zoom factor.
/// basically, each increment of a 'zoom-like' factor should definitely increase the viewable area somehow, even if it isnt strictly by an entire zoom level.
/// </summary>
public Size CalculateClientSize(IVideoProvider videoProvider, int zoom)
{
var arActive = GlobalConfig.DispFixAspectRatio;
var arSystem = GlobalConfig.DispManagerAR == EDispManagerAR.System;
var arCustom = GlobalConfig.DispManagerAR == EDispManagerAR.CustomSize;
var arCustomRatio = GlobalConfig.DispManagerAR == EDispManagerAR.CustomRatio;
var arCorrect = arSystem || arCustom || arCustomRatio;
var arInteger = GlobalConfig.DispFixScaleInteger;
var bufferWidth = videoProvider.BufferWidth;
var bufferHeight = videoProvider.BufferHeight;
var virtualWidth = videoProvider.VirtualWidth;
var virtualHeight = videoProvider.VirtualHeight;
if (arCustom)
{
virtualWidth = GlobalConfig.DispCustomUserARWidth;
virtualHeight = GlobalConfig.DispCustomUserARHeight;
}
if (arCustomRatio)
{
FixRatio(GlobalConfig.DispCustomUserArx, GlobalConfig.DispCustomUserAry,
videoProvider.BufferWidth, videoProvider.BufferHeight, out virtualWidth, out virtualHeight);
}
// TODO: it is bad that this is happening outside the filter chain
// the filter chain has the ability to add padding...
// for now, we have to have some hacks. this could be improved by refactoring the filter setup hacks to be in one place only though
// could the PADDING be done as filters too? that would be nice.
var fCoreScreenControl = CreateCoreScreenControl();
if (fCoreScreenControl != null)
{
var sz = fCoreScreenControl.PresizeInput("default", new(bufferWidth, bufferHeight));
virtualWidth = bufferWidth = sz.Width;
virtualHeight = bufferHeight = sz.Height;
}
var padding = CalculateCompleteContentPaddingSum(true, false);
virtualWidth += padding.Horizontal;
virtualHeight += padding.Vertical;
padding = CalculateCompleteContentPaddingSum(true, true);
bufferWidth += padding.Horizontal;
bufferHeight += padding.Vertical;
// in case the user requested so much padding that the dimensions are now negative, just turn it to something small.
if (virtualWidth < 1) virtualWidth = 1;
if (virtualHeight < 1) virtualHeight = 1;
if (bufferWidth < 1) bufferWidth = 1;
if (bufferHeight < 1) bufferHeight = 1;
// old stuff
var fvp = new FakeVideoProvider(bufferWidth, bufferHeight, virtualWidth, virtualHeight);
Size chainOutsize;
if (arActive)
{
if (arCorrect)
{
if (arInteger)
{
// ALERT COPYPASTE LAUNDROMAT
var AR = new Vector2(virtualWidth / (float) bufferWidth, virtualHeight / (float) bufferHeight);
var targetPar = AR.X / AR.Y;
// this would malfunction for AR <= 0.5 or AR >= 2.0
// EDIT - in fact, we have AR like that coming from PSX, sometimes, so maybe we should solve this better
var PS = Vector2.One; // this would malfunction for AR <= 0.5 or AR >= 2.0
// here's how we define zooming, in this case:
// make sure each step is an increment of zoom for at least one of the dimensions (or maybe both of them)
// look for the increment which helps the AR the best
// TODO - this cant possibly support scale factors like 1.5x
// TODO - also, this might be messing up zooms and stuff, we might need to run this on the output size of the filter chain
Span<Vector2> trials = stackalloc Vector2[3];
for (var i = 1; i < zoom; i++)
{
// would not be good to run this per frame, but it seems to only run when the resolution changes, etc.
trials[0] = PS + Vector2.UnitX;
trials[1] = PS + Vector2.UnitY;
trials[2] = PS + Vector2.One;
var bestIndex = -1;
var bestValue = 1000.0f;
for (var t = 0; t < trials.Length; t++)
{
//I.
var testAr = trials[t].X / trials[t].Y;
// II.
// var calc = Vector2.Multiply(trials[t], VS);
// var test_ar = calc.X / calc.Y;
// not clear which approach is superior
var deviationLinear = Math.Abs(testAr - targetPar);
if (deviationLinear < bestValue)
{
bestIndex = t;
bestValue = deviationLinear;
}
}
// is it possible to get here without selecting one? doubtful.
// EDIT: YES IT IS. it happened with an 0,0 buffer size. of course, that was a mistake, but we shouldn't crash
if (bestIndex != -1) // so, what now? well, this will result in 0,0 getting picked, so that's probably all we can do
{
PS = trials[bestIndex];
}
}
chainOutsize = new((int)(bufferWidth * PS.X), (int)(bufferHeight * PS.Y));
}
else
{
// obey the AR, but allow free scaling: just zoom the virtual size
chainOutsize = new(virtualWidth * zoom, virtualHeight * zoom);
}
}
else
{
// ar_unity:
// just choose to zoom the buffer (make no effort to incorporate AR)
chainOutsize = new(bufferWidth * zoom, bufferHeight * zoom);
}
}
else
{
// !ar_active:
// just choose to zoom the buffer (make no effort to incorporate AR)
chainOutsize = new(bufferWidth * zoom, bufferHeight * zoom);
}
chainOutsize.Width += ClientExtraPadding.Left + ClientExtraPadding.Right;
chainOutsize.Height += ClientExtraPadding.Top + ClientExtraPadding.Bottom;
var job = new JobInfo
{
VideoProvider = fvp,
Simulate = true,
ChainOutsize = chainOutsize,
IncludeUserFilters = true,
IncludeOSD = true,
};
var filterProgram = UpdateSourceInternal(job);
// this only happens when we're forcing the client to size itself with autoload and the core says 0x0....
// we need some other more sensible client size.
if (filterProgram == null)
{
return new(256, 192);
}
var size = filterProgram.Filters[filterProgram.Filters.Count - 1].FindOutput().SurfaceFormat.Size;
return size;
}
protected class JobInfo
{
public IVideoProvider VideoProvider;
public bool Simulate;
public Size ChainOutsize;
public bool Offscreen;
public BitmapBuffer OffscreenBb;
public bool IncludeOSD;
/// <summary>
/// This has been changed a bit to mean "not raw".
/// Someone needs to rename it, but the sense needs to be inverted and some method args need renaming too
/// Suggested: IsRaw (with inverted sense)
/// </summary>
public bool IncludeUserFilters;
}
private FilterProgram UpdateSourceInternal(JobInfo job)
{
// no drawing actually happens
if (!job.Simulate)
{
ActivateGraphicsControlContext();
if (job.ChainOutsize.Width == 0 || job.ChainOutsize.Height == 0)
{
// this has to be a NOP, because lots of stuff will malfunction on a 0-sized viewport
if (_currentFilterProgram != null)
{
UpdateSourceDrawingWork(job); //but we still need to do this, because of vsync
}
return null;
}
}
var videoProvider = job.VideoProvider;
var glTextureProvider = videoProvider as IGLTextureProvider;
var simulate = job.Simulate;
var chainOutsize = job.ChainOutsize;
var bufferWidth = videoProvider.BufferWidth;
var bufferHeight = videoProvider.BufferHeight;
var presenterTextureWidth = bufferWidth;
var presenterTextureHeight = bufferHeight;
var vw = videoProvider.VirtualWidth;
var vh = videoProvider.VirtualHeight;
// TODO: it is bad that this is happening outside the filter chain
// the filter chain has the ability to add padding...
// for now, we have to have some hacks. this could be improved by refactoring the filter setup hacks to be in one place only though
// could the PADDING be done as filters too? that would be nice.
var fCoreScreenControl = CreateCoreScreenControl();
if(fCoreScreenControl != null)
{
var sz = fCoreScreenControl.PresizeInput("default", new(bufferWidth, bufferHeight));
presenterTextureWidth = vw = sz.Width;
presenterTextureHeight = vh = sz.Height;
}
if (GlobalConfig.DispFixAspectRatio)
{
switch (GlobalConfig.DispManagerAR)
{
case EDispManagerAR.None:
vw = bufferWidth;
vh = bufferHeight;
break;
case EDispManagerAR.System:
// Already set
break;
case EDispManagerAR.CustomSize:
// not clear what any of these other options mean for "screen controlled" systems
vw = GlobalConfig.DispCustomUserARWidth;
vh = GlobalConfig.DispCustomUserARHeight;
break;
case EDispManagerAR.CustomRatio:
// not clear what any of these other options mean for "screen controlled" systems
FixRatio(GlobalConfig.DispCustomUserArx, GlobalConfig.DispCustomUserAry, videoProvider.BufferWidth, videoProvider.BufferHeight, out vw, out vh);
break;
default:
throw new InvalidOperationException();
}
}
var padding = CalculateCompleteContentPaddingSum(true,false);
vw += padding.Horizontal;
vh += padding.Vertical;
//in case the user requested so much padding that the dimensions are now negative, just turn it to something small.
if (vw < 1) vw = 1;
if (vh < 1) vh = 1;
BitmapBuffer bb = null;
Texture2d videoTexture = null;
if (!simulate)
{
if (glTextureProvider != null && _gl.DispMethodEnum == EDispMethod.OpenGL)
{
// FYI: this is a million years from happening on n64, since it's all geriatric non-FBO code
videoTexture = _gl.WrapGLTexture2d(new(glTextureProvider.GetGLTexture()), bufferWidth, bufferHeight);
}
else
{
// wrap the VideoProvider data in a BitmapBuffer (no point to refactoring that many IVideoProviders)
bb = new(bufferWidth, bufferHeight, videoProvider.GetVideoBuffer());
bb.DiscardAlpha();
//now, acquire the data sent from the videoProvider into a texture
videoTexture = _videoTextureFrugalizer.Get(bb);
// lets not use this. lets define BizwareGL to make clamp by default (TBD: check opengl)
// _gl.SetTextureWrapMode(videoTexture, true);
}
}
// 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);
var filterProgram = BuildDefaultChain(chainInsize, chainOutsize, job.IncludeOSD, job.IncludeUserFilters);
filterProgram.GuiRenderer = _renderer;
filterProgram.GL = _gl;
//setup the source image filter
var fInput = (SourceImage)filterProgram["input"];
fInput.Texture = videoTexture;
//setup the final presentation filter
var fPresent = (FinalPresentation)filterProgram["presentation"];
if (fPresent != null)
{
fPresent.VirtualTextureSize = new(vw, vh);
fPresent.TextureSize = new(presenterTextureWidth, presenterTextureHeight);
fPresent.BackgroundColor = videoProvider.BackgroundColor;
fPresent.Config_FixAspectRatio = GlobalConfig.DispFixAspectRatio;
fPresent.Config_FixScaleInteger = GlobalConfig.DispFixScaleInteger;
fPresent.Padding = (ClientExtraPadding.Left, ClientExtraPadding.Top, ClientExtraPadding.Right, ClientExtraPadding.Bottom);
}
filterProgram.Compile("default", chainInsize, chainOutsize, !job.Offscreen);
if (simulate)
{
}
else
{
_currentFilterProgram = filterProgram;
UpdateSourceDrawingWork(job);
}
// cleanup:
bb?.Dispose();
return filterProgram;
}
public void Blank()
{
ActivateGraphicsControlContext();
_gl.BeginScene();
_gl.BindRenderTarget(null);
_gl.ClearColor(Color.Black);
_gl.EndScene();
SwapBuffersOfGraphicsControl();
}
protected virtual void UpdateSourceDrawingWork(JobInfo job)
{
if (!job.Offscreen) throw new InvalidOperationException();
// begin rendering on this context
// should this have been done earlier?
// do i need to check this on an intel video card to see if running excessively is a problem? (it used to be in the FinalTarget command below, shouldn't be a problem)
//GraphicsControl.Begin(); // CRITICAL POINT for yabause+GL
//TODO - auto-create and age these (and dispose when old)
var rtCounter = 0;
// ReSharper disable once AccessToModifiedClosure
_currentFilterProgram.RenderTargetProvider = new DisplayManagerRenderTargetProvider(size => _shaderChainFrugalizers[rtCounter++].Get(size));
_gl.BeginScene();
RunFilterChainSteps(ref rtCounter, out var rtCurr, out _);
_gl.EndScene();
job.OffscreenBb = rtCurr.Texture2d.Resolve();
job.OffscreenBb.DiscardAlpha();
}
protected void RunFilterChainSteps(ref int rtCounter, out RenderTarget rtCurr, out bool inFinalTarget)
{
Texture2d texCurr = null;
rtCurr = null;
inFinalTarget = false;
foreach (var step in _currentFilterProgram.Program) switch (step.Type)
{
case FilterProgram.ProgramStepType.Run:
var f = _currentFilterProgram.Filters[(int) step.Args];
f.SetInput(texCurr);
f.Run();
if (f.FindOutput() is { SurfaceDisposition: SurfaceDisposition.Texture })
{
texCurr = f.GetOutput();
rtCurr = null;
}
break;
case FilterProgram.ProgramStepType.NewTarget:
_currentFilterProgram.CurrRenderTarget = rtCurr = _shaderChainFrugalizers[rtCounter++].Get((Size) step.Args);
rtCurr.Bind();
break;
case FilterProgram.ProgramStepType.FinalTarget:
_currentFilterProgram.CurrRenderTarget = rtCurr = null;
_gl.BindRenderTarget(rtCurr);
inFinalTarget = true;
break;
default:
throw new InvalidOperationException();
}
}
private void LoadCustomFont(Stream fontStream)
{
var data = Marshal.AllocCoTaskMem((int)fontStream.Length);
try
{
var fontData = new byte[fontStream.Length];
fontStream.Read(fontData, 0, (int)fontStream.Length);
Marshal.Copy(fontData, 0, data, (int)fontStream.Length);
CustomFonts.AddMemoryFont(data, fontData.Length);
}
finally
{
Marshal.FreeCoTaskMem(data);
fontStream.Close();
}
}
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();
/// <summary>
/// Peeks a locked lua surface, or returns null if it isn't locked
/// </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 void ClearApiHawkSurfaces()
{
foreach (var kvp in _apiHawkSurfaceSets)
{
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
}
}
}
/// <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);
}
// helper classes:
private class MyBlitter : IBlitter
{
private readonly DisplayManagerBase _owner;
public MyBlitter(DisplayManagerBase dispManager)
{
_owner = dispManager;
}
public StringRenderer GetFontType(string fontType) => _owner._theOneFont;
public void DrawString(string s, StringRenderer font, Color color, float x, float y)
{
_owner._renderer.SetModulateColor(color);
font.RenderString(_owner._renderer, x, y, s);
_owner._renderer.SetModulateColorWhite();
}
public SizeF MeasureString(string s, StringRenderer font) => font.Measure(s);
public Rectangle ClipBounds { get; set; }
}
}
}