diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs index b686365d61f..f0e4685fdeb 100644 --- a/src/BizHawk.Client.Common/RomLoader.cs +++ b/src/BizHawk.Client.Common/RomLoader.cs @@ -481,11 +481,6 @@ private void LoadOther( ); return; } - - if (_config.GbAsSgb) - { - game.System = VSystemID.Raw.SGB; - } break; case VSystemID.Raw.PSX when ext is ".bin": const string FILE_EXT_CUE = ".cue"; @@ -851,11 +846,6 @@ private void DispatchErrorMessage(Exception ex, string system, string path) { DoLoadErrorCallback(ex.Message, system, path, Deterministic, LoadErrorType.MissingFirmware); } - else if (ex is CGBNotSupportedException) - { - // failed to load SGB bios or game does not support SGB mode. - DoLoadErrorCallback("Failed to load a GB rom in SGB mode. You might try disabling SGB Mode.", system); - } else if (ex is NoAvailableCoreException) { // handle exceptions thrown by the new detected systems that BizHawk does not have cores for diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index b38ba8dda56..39e9dc635f3 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -25,11 +25,9 @@ public class Config public static readonly IReadOnlyList<(string[] AppliesTo, string[] CoreNames)> CorePickerUIData = new List<(string[], string[])> { ([ VSystemID.Raw.GB, VSystemID.Raw.GBC ], - [ CoreNames.Gambatte, CoreNames.Sameboy, CoreNames.GbHawk, CoreNames.SubGbHawk ]), + [ CoreNames.Gambatte, CoreNames.Sameboy, CoreNames.GbHawk, CoreNames.SubGbHawk, CoreNames.Bsnes, CoreNames.Bsnes115, CoreNames.SubBsnes115 ]), ([ VSystemID.Raw.GBL ], [ CoreNames.GambatteLink, CoreNames.GBHawkLink, CoreNames.GBHawkLink3x, CoreNames.GBHawkLink4x ]), - ([ VSystemID.Raw.SGB ], - [ CoreNames.Gambatte, CoreNames.Bsnes115, CoreNames.SubBsnes115, CoreNames.Bsnes ]), ([ VSystemID.Raw.GEN ], [ CoreNames.Gpgx, CoreNames.PicoDrive ]), ([ VSystemID.Raw.N64 ], @@ -387,8 +385,6 @@ public void SetWindowScaleFor(string sysID, int windowScale) public Dictionary> AllTrollersAnalog { get; set; } = new Dictionary>(); public Dictionary> AllTrollersFeedbacks { get; set; } = new Dictionary>(); - /// as this setting spans multiple cores and doesn't actually affect the behavior of any core, it hasn't been absorbed into the new system - public bool GbAsSgb { get; set; } public string LibretroCore { get; set; } public Dictionary PreferredCores = GenDefaultCorePreferences(); diff --git a/src/BizHawk.Client.Common/movie/import/LsmvImport.cs b/src/BizHawk.Client.Common/movie/import/LsmvImport.cs index 6254cb4af79..54ceb7d17e1 100644 --- a/src/BizHawk.Client.Common/movie/import/LsmvImport.cs +++ b/src/BizHawk.Client.Common/movie/import/LsmvImport.cs @@ -159,8 +159,7 @@ protected override void RunImport() break; case "sgb_ntsc": case "sgb_pal": - platform = VSystemID.Raw.SNES; - Config.GbAsSgb = true; + platform = VSystemID.Raw.SGB; break; } diff --git a/src/BizHawk.Client.Common/movie/import/VbmImport.cs b/src/BizHawk.Client.Common/movie/import/VbmImport.cs index 3226e06a3b9..a0ae4c94307 100644 --- a/src/BizHawk.Client.Common/movie/import/VbmImport.cs +++ b/src/BizHawk.Client.Common/movie/import/VbmImport.cs @@ -122,9 +122,15 @@ recording time in Unix epoch format Result.Movie.HeaderEntries.Add("IsCGBMode", "1"); } + var finalCoreName = Config.PreferredCores[VSystemID.Raw.GB]; if (isSGB) { - Result.Errors.Add("SGB imports are not currently supported"); + platform = VSystemID.Raw.SGB; + if (finalCoreName is not CoreNames.Gambatte) + { + Result.Warnings.Add($"{finalCoreName} doesn't support SGB; will use {CoreNames.Gambatte}"); + finalCoreName = CoreNames.Gambatte; + } } Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform; @@ -276,13 +282,15 @@ A stream of 2-byte bitvectors which indicate which buttons are pressed at each p } else { - Result.Movie.HeaderEntries[HeaderKeys.Core] = Config.PreferredCores[VSystemID.Raw.GB]; - switch (Config.PreferredCores[VSystemID.Raw.GB]) + Result.Movie.HeaderEntries[HeaderKeys.Core] = finalCoreName; + switch (finalCoreName) { case CoreNames.Gambatte: Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(new Gameboy.GambatteSyncSettings { - ConsoleMode = is_GBC ? Gameboy.GambatteSyncSettings.ConsoleModeType.GBC : Gameboy.GambatteSyncSettings.ConsoleModeType.GB, + ConsoleMode = isSGB + ? Gameboy.GambatteSyncSettings.ConsoleModeType.SGB2 + : is_GBC ? Gameboy.GambatteSyncSettings.ConsoleModeType.GBC : Gameboy.GambatteSyncSettings.ConsoleModeType.GB, }); break; case CoreNames.GbHawk: diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index a91a5c4b909..17f88191447 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -117,17 +117,9 @@ ToolStripItem[] CreateWindowSizeFactorSubmenus() CoresSubMenu.DropDownItems.Add(submenu); } CoresSubMenu.DropDownItems.Add(new ToolStripSeparator { AutoSize = true }); - var GBInSGBMenuItem = new ToolStripMenuItem { Text = "GB in SGB" }; - GBInSGBMenuItem.Click += (_, _) => - { - Config.GbAsSgb ^= true; - if (Emulator.SystemId is VSystemID.Raw.GB or VSystemID.Raw.GBC or VSystemID.Raw.SGB) FlagNeedsReboot(); - }; - CoresSubMenu.DropDownItems.Add(GBInSGBMenuItem); var setLibretroCoreToolStripMenuItem = new ToolStripMenuItem { Text = "Set Libretro Core..." }; setLibretroCoreToolStripMenuItem.Click += (_, _) => RunLibretroCoreChooser(); CoresSubMenu.DropDownItems.Add(setLibretroCoreToolStripMenuItem); - CoresSubMenu.DropDownOpened += (_, _) => GBInSGBMenuItem.Checked = Config.GbAsSgb; ToolStripMenuItemEx recentCoreSettingsSubmenu = new() { Text = "Recent" }; recentCoreSettingsSubmenu.DropDownItems.AddRange(CreateCoreSettingsSubmenus().ToArray()); diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/BsnesCore.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/BsnesCore.cs index 0109ea1adb8..e4dfef41128 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/BsnesCore.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/BsnesCore.cs @@ -17,8 +17,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES [ServiceNotApplicable(new[] { typeof(IDriveLight) })] public partial class BsnesCore : IEmulator, IDebuggable, IVideoProvider, ISaveRam, IStatable, IInputPollable, IRegionable, ISettable, IBSNESForGfxDebugger, IBoardInfo { + [CoreConstructor(VSystemID.Raw.GB)] + [CoreConstructor(VSystemID.Raw.GBC)] [CoreConstructor(VSystemID.Raw.Satellaview)] - [CoreConstructor(VSystemID.Raw.SGB)] [CoreConstructor(VSystemID.Raw.SNES)] public BsnesCore(CoreLoadParameters loadParameters) : this(loadParameters, false) { } public BsnesCore(CoreLoadParameters loadParameters, bool subframe = false) @@ -29,8 +30,8 @@ public BsnesCore(CoreLoadParameters loadParamete this._romPath = Path.ChangeExtension(loadParameters.Roms[0].RomPath, null); CoreComm = loadParameters.Comm; _syncSettings = loadParameters.SyncSettings ?? new SnesSyncSettings(); - SystemId = loadParameters.Game.System; - _isSGB = SystemId == VSystemID.Raw.SGB; + _isSGB = loadParameters.Game.System is VSystemID.Raw.GB or VSystemID.Raw.GBC; + SystemId = _isSGB ? VSystemID.Raw.SGB : loadParameters.Game.System; _currentMsuTrack = new ProxiedFile(); byte[] sgbRomData = null; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/SubBsnesCore.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/SubBsnesCore.cs index f84cf5c905e..a66178219ca 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/SubBsnesCore.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/BSNES/SubBsnesCore.cs @@ -8,8 +8,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES [ServiceNotApplicable(new[] { typeof(IDriveLight) })] public class SubBsnesCore : IEmulator, ICycleTiming { + [CoreConstructor(VSystemID.Raw.GB)] + [CoreConstructor(VSystemID.Raw.GBC)] [CoreConstructor(VSystemID.Raw.Satellaview)] - [CoreConstructor(VSystemID.Raw.SGB)] [CoreConstructor(VSystemID.Raw.SNES)] public SubBsnesCore(CoreLoadParameters loadParameters) { diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.ISettable.cs index e80f937d927..874d0926620 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.ISettable.cs @@ -110,11 +110,12 @@ public enum ConsoleModeType Auto, GB, GBC, - GBA + GBA, + SGB2, } [DisplayName("Console Mode")] - [Description("Pick which console to run, 'Auto' chooses from ROM header; 'GB', 'GBC', and 'GBA' chooses the respective system. Does nothing in SGB mode.")] + [Description("Picks which console to emulate.")] [DefaultValue(ConsoleModeType.Auto)] public ConsoleModeType ConsoleMode { get; set; } diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.cs index 2c841e8c224..a218888a637 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.cs @@ -21,8 +21,7 @@ public partial class Gameboy : IInputPollable, IRomInfo, IGameboyCommon, ICycleT [CoreConstructor(VSystemID.Raw.GB)] [CoreConstructor(VSystemID.Raw.GBC)] - [CoreConstructor(VSystemID.Raw.SGB)] - public Gameboy(CoreComm comm, IGameInfo game, byte[] file, GambatteSettings settings, GambatteSyncSettings syncSettings, bool deterministic) + public Gameboy(CoreComm comm, GameInfo game, byte[] file, GambatteSettings settings, GambatteSyncSettings syncSettings, bool deterministic) { _serviceProvider = new(this); _serviceProvider.Register(_disassembler); @@ -58,6 +57,10 @@ public Gameboy(CoreComm comm, IGameInfo game, byte[] file, GambatteSettings sett case GambatteSyncSettings.ConsoleModeType.GBA: flags |= LibGambatte.LoadFlags.CGB_MODE | LibGambatte.LoadFlags.GBA_FLAG; break; + case GambatteSyncSettings.ConsoleModeType.SGB2: + flags |= LibGambatte.LoadFlags.SGB_MODE; + IsSgb = true; + break; case GambatteSyncSettings.ConsoleModeType.Auto: if (game.System == VSystemID.Raw.GBC) flags |= LibGambatte.LoadFlags.CGB_MODE; @@ -66,13 +69,6 @@ public Gameboy(CoreComm comm, IGameInfo game, byte[] file, GambatteSettings sett throw new InvalidOperationException(); } - if (game.System == VSystemID.Raw.SGB) - { - flags &= ~(LibGambatte.LoadFlags.CGB_MODE | LibGambatte.LoadFlags.GBA_FLAG); - flags |= LibGambatte.LoadFlags.SGB_MODE; - IsSgb = true; - } - IsCgb = (flags & LibGambatte.LoadFlags.CGB_MODE) == LibGambatte.LoadFlags.CGB_MODE; bool ForceBios() @@ -636,7 +632,7 @@ public IGPUMemoryAreas LockGPU() Vram = _vram, Oam = _oam, Sppal = _sppal, - Bgpal = _bgpal, + Bgpal = _bgpal, }; } diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IEmulator.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IEmulator.cs index 97e3a1fd721..7549f3e416f 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IEmulator.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IEmulator.cs @@ -1,3 +1,4 @@ +using BizHawk.Common.StringExtensions; using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Nintendo.Gameboy @@ -17,16 +18,26 @@ public bool FrameAdvance(IController controller, bool render, bool rendersound = foreach (var s in GBLinkController.BoolButtons) { - if (controller.IsPressed(s)) + if (!controller.IsPressed(s)) continue; + if (s[0] is not 'P') continue; + var iSpace = s.IndexOf(' '); + var playerNum = int.Parse(s.Substring(startIndex: 1, length: iSpace - 1)); + var consoleNum = 0; + var isSGB = false; + while (consoleNum < _numCores) { - for (int i = 0; i < _numCores; i++) - { - if (s.Contains($"P{i + 1} ")) - { - _linkedConts[i].Set(s.Replace($"P{i + 1} ", "")); - } - } + isSGB = IsSgb(consoleNum); + var playersForConsole = isSGB ? 4 : 1; + if (playerNum <= playersForConsole) break; + playerNum -= playersForConsole; + consoleNum++; } + //TODO rather than this string manipulation, could construct a lookup ahead of time + _linkedConts[consoleNum].Set(s.EndsWithOrdinal("Power") + ? "Power" + : isSGB + ? $"P{playerNum} {s.Substring(startIndex: iSpace + 1)}" + : s.Substring(startIndex: iSpace + 1)); } bool linkDiscoSignalNew = controller.IsPressed("Toggle Link Connection"); @@ -66,10 +77,11 @@ public bool FrameAdvance(IController controller, bool render, bool rendersound = unsafe { - fixed (int* fbuff = &FrameBuffer[0]) + fixed (int* fbuff = &FrameBuffer[0], svbuff = SgbVideoBuffer) { // use pitch to have both cores write to the same frame buffer, interleaved int Pitch = 160 * _numCores; + int sgbPitch = 256 * _numCores; fixed (short* sbuff = &SoundBuffer[0]) { @@ -102,6 +114,27 @@ public bool FrameAdvance(IController controller, bool render, bool rendersound = { Array.Copy(FrameBuffer, (i * 160) + (j * Pitch), VideoBuffer, (i * 160) + (j * Pitch), 160); } + if (IsAnySgb) + { + // all SGB borders will be displayed when any of them has the option enabled + if (IsSgb(i)) + { + if (LibGambatte.gambatte_updatescreenborder( + _linkedCores[i].GambatteState, + svbuff + (i * 256), + sgbPitch) is not 0) + { + throw new InvalidOperationException($"{nameof(LibGambatte.gambatte_updatescreenborder)}() returned non-zero (border error???)"); + } + } + else + { + for (int j = 0; j < 144; j++) + { + Array.Copy(FrameBuffer, (i * 160) + (j * Pitch), SgbVideoBuffer, (i * 256 + 48) + (40 + j) * sgbPitch, 160); + } + } + } } n[i] += (int)nsamp; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IVideoProvider.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IVideoProvider.cs index 178f0f70f44..493fd0ef34d 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IVideoProvider.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IVideoProvider.cs @@ -4,10 +4,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy { public partial class GambatteLink : IVideoProvider { - public int VirtualWidth => 160 * _numCores; - public int VirtualHeight => 144; - public int BufferWidth => 160 * _numCores; - public int BufferHeight => 144; + public int VirtualWidth + => (ShowAnyBorder() ? 256 : 160) * _numCores; + + public int VirtualHeight + => ShowAnyBorder() ? 224 : 144; + + public int BufferWidth + => VirtualWidth; + + public int BufferHeight + => VirtualHeight; public int VsyncNumerator => _linkedCores[P1].VsyncNumerator; @@ -17,13 +24,16 @@ public partial class GambatteLink : IVideoProvider private readonly int[] FrameBuffer; - public int[] GetVideoBuffer() => VideoBuffer; + public int[] GetVideoBuffer() + => ShowAnyBorder() ? SgbVideoBuffer : VideoBuffer; private readonly int[] VideoBuffer; - + + private readonly int[] SgbVideoBuffer; + private int[] CreateVideoBuffer() { - var b = new int[BufferWidth * BufferHeight]; + var b = new int[160 * _numCores * 144]; for (int i = 0; i < b.Length; i++) { b[i] = -1; // GB/C screen is disabled on bootup, so it always starts as white, not black @@ -31,5 +41,7 @@ private int[] CreateVideoBuffer() return b; } + private int[] CreateSGBVideoBuffer() + => IsAnySgb ? new int[256 * _numCores * 224] : null; } } diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.cs index e66dc3b3c89..fc858b1bc11 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.cs @@ -49,7 +49,8 @@ public GambatteLink(CoreLoadParameters pfx + s)); + } ControllerDefinition ret = new($"GB Link {_numCores}x Controller"); - for (int i = 0; i < _numCores; i++) + for (int i = 0, p = 1; i < _numCores; i++) { - ret.BoolButtons.AddRange( - new[] { "Up", "Down", "Left", "Right", "A", "B", "Select", "Start", "Power" } - .Select(s => $"P{i + 1} {s}")); + AddGBButtonsForPlayer(p, ret); + ret.BoolButtons.Add($"P{p} Power"); + p++; + if (IsSgb(i)) + { + // add 3 more gamepads without a Power button + for (int e = p + 3; p < e; p++) AddGBButtonsForPlayer(p, ret); + } } ret.BoolButtons.Add("Toggle Link Connection"); if (_numCores > 2) @@ -159,6 +172,20 @@ private ControllerDefinition CreateControllerDefinition() return ret.MakeImmutable(); } + private bool IsSgb(int i) + => _linkedCores[i].IsSgb; + + public bool IsAnySgb { get; private set; } + + private bool ShowBorder(int i) + => IsSgb(i) && _settings._linkedSettings[i].ShowBorder; + + public bool ShowAnyBorder() + { + for (var i = 0; i < _numCores; i++) if (ShowBorder(i)) return true; + return false; + } + private const int P1 = 0; private const int P2 = 1; private const int P3 = 2; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibGambatte.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibGambatte.cs index 58f42decdfb..c0fdb100381 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibGambatte.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibGambatte.cs @@ -95,6 +95,8 @@ public enum CDLog_Flags : int [DllImport("libgambatte", CallingConvention = CallingConvention.Cdecl)] public static extern int gambatte_updatescreenborder(IntPtr core, int[] videobuf, int pitch); + [DllImport("libgambatte", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int gambatte_updatescreenborder(IntPtr core, int* videobuf, int pitch); [DllImport("libgambatte", CallingConvention = CallingConvention.Cdecl)] public static extern int gambatte_generatesgbsamples(IntPtr core, short[] soundbuf, out uint samples); diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES/LibsnesCore.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES/LibsnesCore.cs index 0cc511b3c77..923716f85f8 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES/LibsnesCore.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES/LibsnesCore.cs @@ -22,7 +22,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.SNES public unsafe partial class LibsnesCore : IEmulator, IVideoProvider, ISaveRam, IStatable, IInputPollable, IRegionable, ICodeDataLogger, IDebuggable, ISettable, IBSNESForGfxDebugger { - [CoreConstructor(VSystemID.Raw.SGB)] + [CoreConstructor(VSystemID.Raw.GB)] + [CoreConstructor(VSystemID.Raw.GBC)] [CoreConstructor(VSystemID.Raw.SNES)] public LibsnesCore(GameInfo game, byte[] rom, CoreComm comm, LibsnesCore.SnesSettings settings, LibsnesCore.SnesSyncSettings syncSettings) @@ -44,8 +45,9 @@ public LibsnesCore(GameInfo game, byte[] romData, byte[] xmlData, string baseRom _game = game; CoreComm = comm; byte[] sgbRomData = null; + IsSGB = game.System is VSystemID.Raw.GB or VSystemID.Raw.GBC; - if (game.System == VSystemID.Raw.SGB) + if (IsSGB) { if ((romData[0x143] & 0xc0) == 0xc0) { @@ -109,9 +111,8 @@ public LibsnesCore(GameInfo game, byte[] romData, byte[] xmlData, string baseRom romData = newData; } - if (game.System == VSystemID.Raw.SGB) + if (IsSGB) { - IsSGB = true; SystemId = VSystemID.Raw.SNES; ser.Register(new SGBBoardInfo());