Rework GB GPU memory areas API (#2566)

* Rework GB GPU memory areas API

All cores can easily implement it now with no copying or awkward garbage.  Also fix the scanline callback and printer callback in Sameboy, which had been broken for some time.

Fixes #2564
This commit is contained in:
nattthebear 2021-01-14 19:17:40 -05:00 committed by GitHub
parent e71506ac6a
commit 0b432994df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 249 additions and 147 deletions

View File

@ -32,13 +32,24 @@ namespace BizHawk.Client.EmuHawk
// g' = 8.25g
// b' = 8.25b
private GPUMemoryAreas _memory;
private bool _cgb; // set once at start
private int _lcdc; // set at each callback
private IntPtr _tilesPal; // current palette to use on tiles
/// <summary>
/// Whether the tiles are being drawn with the sprite or bg palettes
/// </summary>
private bool _tilesPalIsSprite;
/// <summary>
/// How far (in bytes, I guess?) we should offset into the tiles palette
/// </summary>
private int _tilesPalOffset;
private IntPtr ComputeTilesPalFromMemory(IGPUMemoryAreas m)
{
var ret = _tilesPalIsSprite ? m.Sppal : m.Bgpal;
ret += _tilesPalOffset;
return ret;
}
private Color _spriteback;
@ -86,9 +97,6 @@ namespace BizHawk.Client.EmuHawk
{
_cgb = Gb.IsCGBMode();
_lcdc = 0;
_memory = Gb.GetGPU();
_tilesPal = _memory.Bgpal;
label4.Enabled = _cgb;
bmpViewBG.Clear();
@ -420,17 +428,19 @@ namespace BizHawk.Client.EmuHawk
private void ScanlineCallback(byte lcdc)
{
using (_memory.EnterExit())
using (var memory = Gb.LockGPU())
{
var bgPal = _memory.Bgpal;
var spPal = _memory.Sppal;
var oam = _memory.Oam;
var vram = _memory.Vram;
var bgPal = memory.Bgpal;
var spPal = memory.Sppal;
var oam = memory.Oam;
var vram = memory.Vram;
var tilesPal = ComputeTilesPalFromMemory(memory);
_lcdc = lcdc;
// set alpha on all pixels
#if false
// TODO: RE: Spriteback, you can't muck with Sameboy in this way due to how SGB reads stuff...?
// TODO: This probably shouldn't be done on any cores at all. Let the tool make a separate copy of palettes if it needs alpha,
// or compel the cores to send data with alpha already set. What was this actually solving?
unsafe
{
int* p = (int*)_bgpal;
@ -484,11 +494,11 @@ namespace BizHawk.Client.EmuHawk
// tile display
// TODO: user selects palette to use, instead of fixed palette 0
// or possibly "smart" where, if a tile is in use, it's drawn with one of the palettes actually being used with it?
DrawTiles(bmpViewTiles1.Bmp, vram, _tilesPal);
DrawTiles(bmpViewTiles1.Bmp, vram, tilesPal);
bmpViewTiles1.Refresh();
if (_cgb)
{
DrawTiles(bmpViewTiles2.Bmp, vram + 0x2000, _tilesPal);
DrawTiles(bmpViewTiles2.Bmp, vram + 0x2000, tilesPal);
bmpViewTiles2.Refresh();
}
@ -666,10 +676,10 @@ namespace BizHawk.Client.EmuHawk
private unsafe void PaletteMouseover(int x, int y, bool sprite)
{
using (_memory.EnterExit())
using (var memory = Gb.LockGPU())
{
var bgPal = _memory.Bgpal;
var spPal = _memory.Sppal;
var bgPal = memory.Bgpal;
var spPal = memory.Sppal;
bmpViewDetails.ChangeBitmapSize(8, 10);
if (bmpViewDetails.Height != 80)
@ -712,9 +722,10 @@ namespace BizHawk.Client.EmuHawk
private unsafe void TileMouseover(int x, int y, bool secondBank)
{
using (_memory.EnterExit())
using (var memory = Gb.LockGPU())
{
var vram = _memory.Vram;
var vram = memory.Vram;
var tilesPal = ComputeTilesPalFromMemory(memory);
// todo: draw with a specific palette
bmpViewDetails.ChangeBitmapSize(8, 8);
@ -730,7 +741,7 @@ namespace BizHawk.Client.EmuHawk
: $"Tile #{tileIndex} @{tileOffset + 0x8000:x4}");
var lockData = bmpViewDetails.Bmp.LockBits(new Rectangle(0, 0, 8, 8), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
DrawTile((byte*)vram + tileOffset + (secondBank ? 8192 : 0), (int*)lockData.Scan0, lockData.Stride / sizeof(int), (int*)_tilesPal);
DrawTile((byte*)vram + tileOffset + (secondBank ? 8192 : 0), (int*)lockData.Scan0, lockData.Stride / sizeof(int), (int*)tilesPal);
bmpViewDetails.Bmp.UnlockBits(lockData);
labelDetails.Text = sb.ToString();
bmpViewDetails.Refresh();
@ -739,10 +750,10 @@ namespace BizHawk.Client.EmuHawk
private unsafe void TileMapMouseover(int x, int y, bool win)
{
using (_memory.EnterExit())
using (var memory = Gb.LockGPU())
{
var _bgpal = _memory.Bgpal;
var _vram = _memory.Vram;
var _bgpal = memory.Bgpal;
var _vram = memory.Vram;
bmpViewDetails.ChangeBitmapSize(8, 8);
if (bmpViewDetails.Height != 64)
@ -784,11 +795,11 @@ namespace BizHawk.Client.EmuHawk
private unsafe void SpriteMouseover(int x, int y)
{
using (_memory.EnterExit())
using (var memory = Gb.LockGPU())
{
var spPal = _memory.Sppal;
var oam = _memory.Oam;
var vram = _memory.Vram;
var spPal = memory.Sppal;
var oam = memory.Oam;
var vram = memory.Vram;
bool tall = _lcdc.Bit(2);
x /= 8;
@ -974,13 +985,21 @@ namespace BizHawk.Client.EmuHawk
private void bmpView_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
SetFreeze();
}
else if (e.Button == MouseButtons.Left)
{
if (sender == bmpViewBGPal)
_tilesPal = _memory.Bgpal + e.X / 16 * 16;
{
_tilesPalIsSprite = false;
_tilesPalOffset = e.X / 16 * 16;
}
else if (sender == bmpViewSPPal)
_tilesPal = _memory.Sppal + e.X / 16 * 16;
{
_tilesPalIsSprite = true;
_tilesPalOffset = e.X / 16 * 16;
}
}
}

View File

@ -0,0 +1,4 @@
namespace System.Runtime.CompilerServices
{
public static class IsExternalInit {}
}

View File

@ -528,7 +528,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
if (Core._scanlineCallback != null)
{
Core.GetGPU();
Core._scanlineCallback(LCDC);
}
}

View File

@ -529,7 +529,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
if (Core._scanlineCallback != null)
{
Core.GetGPU();
Core._scanlineCallback(LCDC);
}
}

View File

@ -81,7 +81,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
if (_scanlineCallbackLine == -1)
{
GetGPU();
LockGPU();
_scanlineCallback(ppu.LCDC);
}
}
@ -425,11 +425,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
public void Dispose()
{
Marshal.FreeHGlobal(iptr0);
Marshal.FreeHGlobal(iptr1);
Marshal.FreeHGlobal(iptr2);
Marshal.FreeHGlobal(iptr3);
audio.DisposeSound();
}

View File

@ -224,11 +224,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
cpu.SetCallbacks(ReadMemory, PeekMemory, PeekMemory, WriteMemory);
HardReset();
iptr0 = Marshal.AllocHGlobal(VRAM.Length + 1);
iptr1 = Marshal.AllocHGlobal(OAM.Length + 1);
iptr2 = Marshal.AllocHGlobal(ppu.color_palette.Length * 8 * 8 + 1);
iptr3 = Marshal.AllocHGlobal(ppu.color_palette.Length * 8 * 8 + 1);
_scanlineCallback = null;
DeterministicEmulation = true;
@ -236,54 +231,93 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
public bool IsCGBMode() => is_GBC;
public IntPtr iptr0 = IntPtr.Zero;
public IntPtr iptr1 = IntPtr.Zero;
public IntPtr iptr2 = IntPtr.Zero;
public IntPtr iptr3 = IntPtr.Zero;
private GPUMemoryAreas _gpuMemory
/// <summary>
/// Produces a palette in the form that certain frontend inspection tools.
/// May or may not return a reference to the core's own palette, so please don't mutate.
/// </summary>
private uint[] SynthesizeFrontendBGPal()
{
get
if (is_GBC)
{
Marshal.Copy(VRAM, 0, iptr0, VRAM.Length);
Marshal.Copy(OAM, 0, iptr1, OAM.Length);
if (is_GBC)
{
int[] cp2 = new int[32];
int[] cp = new int[32];
for (int i = 0; i < 32; i++)
{
cp2[i] = (int)ppu.OBJ_palette[i];
cp[i] = (int)ppu.BG_palette[i];
}
Marshal.Copy(cp2, 0, iptr2, ppu.OBJ_palette.Length);
Marshal.Copy(cp, 0, iptr3, ppu.BG_palette.Length);
}
else
{
int[] cp2 = new int[8];
for (int i = 0; i < 4; i++)
{
cp2[i] = (int)ppu.color_palette[(ppu.obj_pal_0 >> (i * 2)) & 3];
cp2[i + 4] = (int)ppu.color_palette[(ppu.obj_pal_1 >> (i * 2)) & 3];
}
Marshal.Copy(cp2, 0, iptr2, cp2.Length);
int[] cp = new int[4];
for (int i = 0; i < 4; i++)
{
cp[i] = (int)ppu.color_palette[(ppu.BGP >> (i * 2)) & 3];
}
Marshal.Copy(cp, 0, iptr3, cp.Length);
}
return new GPUMemoryAreas(iptr0, iptr1, iptr2, iptr3);
return ppu.BG_palette;
}
}
else
{
var scratch = new uint[4];
for (int i = 0; i < 4; i++)
{
scratch[i] = ppu.color_palette[(ppu.BGP >> (i * 2)) & 3];
}
return scratch;
}
}
public GPUMemoryAreas GetGPU() => _gpuMemory;
/// <summary>
/// Produces a palette in the form that certain frontend inspection tools.
/// May or may not return a reference to the core's own palette, so please don't mutate.
/// </summary>
private uint[] SynthesizeFrontendSPPal()
{
if (is_GBC)
{
return ppu.OBJ_palette;
}
else
{
var scratch = new uint[8];
for (int i = 0; i < 4; i++)
{
scratch[i] = ppu.color_palette[(ppu.obj_pal_0 >> (i * 2)) & 3];
scratch[i + 4] = ppu.color_palette[(ppu.obj_pal_1 >> (i * 2)) & 3];
}
return scratch;
}
}
public IGPUMemoryAreas LockGPU()
{
return new GPUMemoryAreas(
VRAM,
OAM,
SynthesizeFrontendSPPal(),
SynthesizeFrontendBGPal()
);
}
private class GPUMemoryAreas : IGPUMemoryAreas
{
public IntPtr Vram { get; }
public IntPtr Oam { get; init; }
public IntPtr Sppal { get; init; }
public IntPtr Bgpal { get; init; }
private readonly List<GCHandle> _handles = new();
public GPUMemoryAreas(byte[] vram, byte[] oam, uint[] sppal, uint[] bgpal)
{
Vram = AddHandle(vram);
Oam = AddHandle(oam);
Sppal = AddHandle(sppal);
Bgpal = AddHandle(bgpal);
}
private IntPtr AddHandle(object target)
{
var handle = GCHandle.Alloc(target, GCHandleType.Pinned);
_handles.Add(handle);
return handle.AddrOfPinnedObject();
}
public void Dispose()
{
foreach (var h in _handles)
h.Free();
_handles.Clear();
}
}
public ScanlineCallback _scanlineCallback;
public int _scanlineCallbackLine = 0;
@ -295,7 +329,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
if (line == -2)
{
GetGPU();
LockGPU();
_scanlineCallback(ppu.LCDC);
}
}

View File

@ -151,7 +151,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
{
if (Core._scanlineCallback != null)
{
Core.GetGPU();
Core._scanlineCallback(LCDC);
}
}

View File

@ -99,11 +99,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
GambatteState = IntPtr.Zero;
}
_vram = IntPtr.Zero;
_oam = IntPtr.Zero;
_sppal = IntPtr.Zero;
_bgpal = IntPtr.Zero;
DisposeSound();
}
}

View File

@ -382,13 +382,12 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
}
}
public IntPtr _vram = IntPtr.Zero;
public IntPtr _bgpal = IntPtr.Zero;
public IntPtr _sppal = IntPtr.Zero;
public IntPtr _oam = IntPtr.Zero;
public GPUMemoryAreas GetGPU()
{
public IGPUMemoryAreas LockGPU()
{
var _vram = IntPtr.Zero;
var _bgpal = IntPtr.Zero;
var _sppal = IntPtr.Zero;
var _oam = IntPtr.Zero;
int unused = 0;
if (!LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.vram, ref _vram, ref unused)
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.bgpal, ref _bgpal, ref unused)
@ -397,7 +396,26 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
throw new InvalidOperationException("Unexpected error in gambatte_getmemoryarea");
}
return new GPUMemoryAreas(_vram, _oam, _sppal, _bgpal);
return new GPUMemoryAreas
{
Vram = _vram,
Oam = _oam,
Sppal = _sppal,
Bgpal = _bgpal,
};
}
private class GPUMemoryAreas : IGPUMemoryAreas
{
public IntPtr Vram { get; init; }
public IntPtr Oam { get; init; }
public IntPtr Sppal { get; init; }
public IntPtr Bgpal { get; init; }
public void Dispose() {}
}
/// <summary>

View File

@ -17,7 +17,13 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
public interface IGameboyCommon : ISpecializedEmulatorService
{
bool IsCGBMode();
GPUMemoryAreas GetGPU();
/// <summary>
/// Acquire GPU memory for inspection. The returned object must be disposed as soon as the frontend
/// tool is done inspecting it, and the pointers become invalid once it is disposed.
/// </summary>
/// <returns></returns>
IGPUMemoryAreas LockGPU();
/// <summary>
/// set up callback
@ -32,32 +38,11 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
void SetPrinterCallback(PrinterCallback callback);
}
public class GPUMemoryAreas : IMonitor
public interface IGPUMemoryAreas : IDisposable
{
public IntPtr Vram { get; }
public IntPtr Oam { get; }
public IntPtr Sppal { get; }
public IntPtr Bgpal { get; }
private readonly IMonitor _monitor;
public GPUMemoryAreas(IntPtr vram, IntPtr oam, IntPtr sppal, IntPtr bgpal, IMonitor monitor = null)
{
Vram = vram;
Oam = oam;
Sppal = sppal;
Bgpal = bgpal;
_monitor = monitor;
}
public void Enter()
{
_monitor?.Enter();
}
public void Exit()
{
_monitor?.Exit();
}
IntPtr Vram { get; }
IntPtr Oam { get; }
IntPtr Sppal { get; }
IntPtr Bgpal { get; }
}
}

View File

@ -35,6 +35,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
private readonly bool _cgb;
private readonly bool _sgb;
private readonly IntPtr[] _cachedGpuPointers = new IntPtr[4];
[CoreConstructor("SGB")]
public Sameboy(byte[] rom, CoreComm comm, Settings settings, SyncSettings syncSettings, bool deterministic)
: this(rom, comm, true, settings, syncSettings, deterministic)
@ -59,6 +61,9 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
SystemId = sgb ? "SGB" : "GB"
})
{
_corePrinterCallback = PrinterCallbackRelay;
_coreScanlineCallback = ScanlineCallbackRelay;
_core = PreInit<LibSameboy>(new WaterboxOptions
{
Filename = "sameboy.wbx",
@ -69,7 +74,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
MmapHeapSizeKB = 1024,
SkipCoreConsistencyCheck = comm.CorePreferences.HasFlag(CoreComm.CorePreferencesFlags.WaterboxCoreConsistencyCheck),
SkipMemoryConsistencyCheck = comm.CorePreferences.HasFlag(CoreComm.CorePreferencesFlags.WaterboxMemoryConsistencyCheck),
});
}, new Delegate[] { _corePrinterCallback, _coreScanlineCallback });
_cgb = (rom[0x143] & 0xc0) == 0xc0 && !sgb;
_sgb = sgb;
@ -96,11 +101,9 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
_exe.RemoveReadonlyFile("game.rom");
_exe.RemoveReadonlyFile("boot.rom");
PostInit();
_core.GetGpuMemory(_cachedGpuPointers);
var scratch = new IntPtr[4];
_core.GetGpuMemory(scratch);
_gpuMemory = new GPUMemoryAreas(scratch[0], scratch[1], scratch[3], scratch[2], _exe);
PostInit();
DeterministicEmulation = deterministic || !_syncSettings.UseRealTime;
InitializeRtc(_syncSettings.InitialTime);
@ -273,8 +276,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
protected override unsafe void FrameAdvancePost()
{
if (_scanlineCallback != null && _scanlineCallbackLine == -1)
_scanlineCallback(_core.GetIoReg(0x40));
if (_frontendScanlineCallback != null && _scanlineCallbackLine == -1)
_frontendScanlineCallback(_core.GetIoReg(0x40));
if (_sgb && !_settings.ShowSgbBorder)
{
@ -299,27 +302,73 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
protected override void LoadStateBinaryInternal(BinaryReader reader)
{
UpdateCoreScanlineCallback(false);
_core.SetPrinterCallback(_printerCallback);
_core.SetPrinterCallback(_corePrinterCallback);
}
public bool IsCGBMode() => _cgb;
private readonly GPUMemoryAreas _gpuMemory;
public IGPUMemoryAreas LockGPU()
{
_exe.Enter();
try
{
return new GPUMemoryAreas(_exe)
{
Vram = _cachedGpuPointers[0],
Oam = _cachedGpuPointers[1],
Sppal = _cachedGpuPointers[3],
Bgpal = _cachedGpuPointers[2]
};
}
catch
{
_exe.Exit();
throw;
}
}
public GPUMemoryAreas GetGPU() => _gpuMemory;
private ScanlineCallback _scanlineCallback;
private class GPUMemoryAreas : IGPUMemoryAreas
{
private IMonitor _monitor;
public IntPtr Vram { get; init; }
public IntPtr Oam { get; init; }
public IntPtr Sppal { get; init; }
public IntPtr Bgpal { get; init; }
public GPUMemoryAreas(IMonitor monitor)
{
_monitor = monitor;
}
public void Dispose()
{
_monitor?.Exit();
_monitor = null;
}
}
private readonly ScanlineCallback _coreScanlineCallback;
private ScanlineCallback _frontendScanlineCallback;
private int _scanlineCallbackLine;
private void ScanlineCallbackRelay(byte lcdc)
{
_frontendScanlineCallback?.Invoke(lcdc);
}
public void SetScanlineCallback(ScanlineCallback callback, int line)
{
_scanlineCallback = callback;
_frontendScanlineCallback = callback;
_scanlineCallbackLine = line;
UpdateCoreScanlineCallback(true);
}
private void UpdateCoreScanlineCallback(bool now)
{
if (_scanlineCallback == null)
if (_frontendScanlineCallback == null)
{
_core.SetScanlineCallback(null, -1);
}
@ -327,25 +376,31 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy
{
if (_scanlineCallbackLine >= 0 && _scanlineCallbackLine <= 153)
{
_core.SetScanlineCallback(_scanlineCallback, _scanlineCallbackLine);
_core.SetScanlineCallback(_coreScanlineCallback, _scanlineCallbackLine);
}
else
{
_core.SetScanlineCallback(null, -1);
if (_scanlineCallbackLine == -2 && now)
{
_scanlineCallback(_core.GetIoReg(0x40));
_frontendScanlineCallback(_core.GetIoReg(0x40));
}
}
}
}
private PrinterCallback _printerCallback;
private readonly PrinterCallback _corePrinterCallback;
private PrinterCallback _frontendPrinterCallback;
private void PrinterCallbackRelay(IntPtr image, byte height, byte top_margin, byte bottom_margin, byte exposure)
{
_frontendPrinterCallback?.Invoke(image, height, top_margin, bottom_margin, exposure);
}
public void SetPrinterCallback(PrinterCallback callback)
{
_printerCallback = callback;
_core.SetPrinterCallback(callback);
_frontendPrinterCallback = callback;
_core.SetPrinterCallback(callback != null ? _corePrinterCallback : null);
}
}
}