BSNESv115: add dedicated subframe core (#3281)

* BSNESv115: allow subframe inputs

* BSNESv115: Implement ICycleTiming

may be correct, not sure

* BSNESv115: add dedicated subframe core

I have probably overlooked something...

* Don't implement ICycleTiming in the non-subframe core

requires re-implementing the "FrameAdvance" function in the subframe core

* Register previously missing services in the subframe core as well

* Wire up SubBsnes everywhere in the frontend

* Change reset cycle to reset instruction

* Deduplicate some code

* Slightly rework frame advance logic. The main intent here is to prevent a case where two frames are ran within a single "frame." The current code probably wouldn't crash due to that, but best not to do that.
Also make SGB work here. A bit of a joke since you really can only abuse it for subframe resets, but might as well especially with the settings implying it's possible (and someone is bound to complain).

Co-authored-by: CasualPokePlayer <50538166+CasualPokePlayer@users.noreply.github.com>
This commit is contained in:
Moritz Bender 2022-08-10 23:08:44 +02:00 committed by GitHub
parent 1a27aae45b
commit 929432086f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 206 additions and 43 deletions

Binary file not shown.

View File

@ -235,9 +235,9 @@ namespace BizHawk.Client.Common
s.ShowOBJ_3 = GetSetting(args, 7);
core.PutSettings(s);
}
void SetBsnes(BsnesCore core)
void SetBsnes(ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings> settingsProvider)
{
var s = core.GetSettings();
var s = settingsProvider.GetSettings();
// TODO: This should probably support both prios inidividually but I have no idea whether changing this breaks anything
s.ShowBG1_0 = s.ShowBG1_1 = GetSetting(args, 0);
s.ShowBG2_0 = s.ShowBG2_1 = GetSetting(args, 1);
@ -247,7 +247,7 @@ namespace BizHawk.Client.Common
s.ShowOBJ_1 = GetSetting(args, 5);
s.ShowOBJ_2 = GetSetting(args, 6);
s.ShowOBJ_3 = GetSetting(args, 7);
core.PutSettings(s);
settingsProvider.PutSettings(s);
}
void SetCygne(WonderSwan ws)
{
@ -313,6 +313,9 @@ namespace BizHawk.Client.Common
case BsnesCore bsnes:
SetBsnes(bsnes);
break;
case SubBsnesCore subBsnes:
SetBsnes(subBsnes.ServiceProvider.GetService<ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings>>());
break;
case NES nes:
SetNesHawk(nes);
break;

View File

@ -27,11 +27,11 @@ namespace BizHawk.Client.Common
(new[] { VSystemID.Raw.NES },
new[] { CoreNames.QuickNes, CoreNames.NesHawk, CoreNames.SubNesHawk }),
(new[] { VSystemID.Raw.SNES },
new[] { CoreNames.Faust, CoreNames.Snes9X, CoreNames.Bsnes, CoreNames.Bsnes115 }),
new[] { CoreNames.Faust, CoreNames.Snes9X, CoreNames.Bsnes, CoreNames.Bsnes115, CoreNames.SubBsnes115 }),
(new[] { VSystemID.Raw.N64 },
new[] { CoreNames.Mupen64Plus, CoreNames.Ares64 }),
(new[] { VSystemID.Raw.SGB },
new[] { CoreNames.Gambatte, CoreNames.Bsnes, CoreNames.Bsnes115}),
new[] { CoreNames.Gambatte, CoreNames.Bsnes, CoreNames.Bsnes115, CoreNames.SubBsnes115 }),
(new[] { VSystemID.Raw.GB, VSystemID.Raw.GBC },
new[] { CoreNames.Gambatte, CoreNames.Sameboy, CoreNames.GbHawk, CoreNames.SubGbHawk }),
(new[] { VSystemID.Raw.GBL },

View File

@ -244,7 +244,9 @@ namespace BizHawk.Client.Common
["Extra1"] = '1',
["Extra2"] = '2',
["Extra3"] = '3',
["Extra4"] = '4'
["Extra4"] = '4',
["Subframe"] = 'F'
},
[VSystemID.Raw.TI83] = new()
{

View File

@ -1723,6 +1723,7 @@ namespace BizHawk.Client.EmuHawk
{
LibsnesCore => OpenOldBSNESGamepadSettingsDialog(GetSettingsAdapterForLoadedCore<LibsnesCore>()),
BsnesCore => OpenBSNESGamepadSettingsDialog(GetSettingsAdapterForLoadedCore<BsnesCore>()),
SubBsnesCore => OpenBSNESGamepadSettingsDialog(GetSettingsAdapterForLoadedCore<SubBsnesCore>()),
_ => DialogResult.None
};
}
@ -1744,6 +1745,7 @@ namespace BizHawk.Client.EmuHawk
{
LibsnesCore => OpenOldBSNESSettingsDialog(GetSettingsAdapterForLoadedCore<LibsnesCore>()),
BsnesCore => OpenBSNESSettingsDialog(GetSettingsAdapterForLoadedCore<BsnesCore>()),
SubBsnesCore => OpenBSNESSettingsDialog(GetSettingsAdapterForLoadedCore<SubBsnesCore>()),
_ => DialogResult.None
};
}
@ -2774,6 +2776,13 @@ namespace BizHawk.Client.EmuHawk
bsnesSubmenu.DropDownOpened += (_, _) => bsnesGamepadSettingsItem.Enabled = MovieSession.Movie.NotActive() || Emulator is not BsnesCore;
items.Add(bsnesSubmenu);
// SubBSNESv115+
var subBsnesGamepadSettingsItem = CreateSettingsItem("Controller Configuration...", (_, _) => OpenBSNESGamepadSettingsDialog(GetSettingsAdapterFor<SubBsnesCore>()));
var subBsnesSettingsItem = CreateSettingsItem("Options...", (_, _) => OpenBSNESSettingsDialog(GetSettingsAdapterFor<SubBsnesCore>()));
var subBsnesSubmenu = CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.SubBsnes115, subBsnesGamepadSettingsItem, subBsnesSettingsItem);
subBsnesSubmenu.DropDownOpened += (_, _) => subBsnesGamepadSettingsItem.Enabled = MovieSession.Movie.NotActive() || Emulator is not SubBsnesCore;
items.Add(subBsnesSubmenu);
// C64Hawk
items.Add(CreateCoreSubmenu(VSystemCategory.PCs, CoreNames.C64Hawk, CreateSettingsItem("Settings...", (_, _) => OpenC64HawkSettingsDialog())));

View File

@ -1478,7 +1478,7 @@ namespace BizHawk.Client.EmuHawk
private void SNES_ToggleBg(int layer)
{
if (Emulator is not (BsnesCore or LibsnesCore or Snes9x) || !1.RangeTo(4).Contains(layer))
if (Emulator is not (BsnesCore or SubBsnesCore or LibsnesCore or Snes9x) || !1.RangeTo(4).Contains(layer))
{
return;
}
@ -1486,9 +1486,10 @@ namespace BizHawk.Client.EmuHawk
bool result = false;
switch (Emulator)
{
case BsnesCore bsnes:
case BsnesCore or SubBsnesCore:
{
var s = bsnes.GetSettings();
var settingsProvider = Emulator.ServiceProvider.GetService<ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings>>();
var s = settingsProvider.GetSettings();
switch (layer)
{
case 1:
@ -1505,7 +1506,7 @@ namespace BizHawk.Client.EmuHawk
break;
}
bsnes.PutSettings(s);
settingsProvider.PutSettings(s);
break;
}
case LibsnesCore libsnes:
@ -2057,7 +2058,12 @@ namespace BizHawk.Client.EmuHawk
SnesGfxDebuggerMenuItem.Visible = true;
break;
case VSystemID.Raw.SNES when Emulator is BsnesCore bsnesCore:
SNESSubMenu.Text = bsnesCore.IsSGB ? "&SGB" : "&SNES";
SNESSubMenu.Text = bsnesCore.IsSGB ? "&SGB" : "&SNES";
SnesGfxDebuggerMenuItem.Visible = false;
SNESSubMenu.Visible = true;
break;
case VSystemID.Raw.SNES when Emulator is SubBsnesCore subBsnesCore:
SNESSubMenu.Text = subBsnesCore.IsSGB ? "&SGB" : "&SNES";
SnesGfxDebuggerMenuItem.Visible = false;
SNESSubMenu.Visible = true;
break;

View File

@ -47,7 +47,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
[BizImport(CallingConvention.Cdecl)]
public abstract void snes_reset();
[BizImport(CallingConvention.Cdecl)]
public abstract void snes_run();
public abstract bool snes_run(bool breakOnLatch);
[BizImport(CallingConvention.Cdecl)]
public abstract void snes_serialize(byte[] serializedData, int serializedSize);
@ -68,6 +68,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
[BizImport(CallingConvention.Cdecl)]
public abstract bool snes_cpu_step();
[BizImport(CallingConvention.Cdecl)]
public abstract long snes_get_executed_cycles();
[BizImport(CallingConvention.Cdecl)]
public abstract bool snes_msu_sync();
}

View File

@ -35,7 +35,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
public ControllerDefinition Definition { get; }
public BsnesControllers(BsnesCore.SnesSyncSettings ss)
public BsnesControllers(BsnesCore.SnesSyncSettings ss, bool subframe = false)
{
_ports = new[]
{
@ -52,6 +52,13 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
// add buttons that the core itself will handle
Definition.BoolButtons.Add("Reset");
Definition.BoolButtons.Add("Power");
if (subframe)
{
// When set, only emulate until the next input latch (or until the frame ends)
Definition.BoolButtons.Add("Subframe");
// Amount of instructions to execute before resetting; range hopefully set large enough
Definition.AddAxis("Reset Instruction", 0.RangeTo(200000), 0);
}
Definition.MakeImmutable();
}

View File

@ -89,12 +89,11 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
}
}
public long TotalExecutedCycles { get; private set; }
public long TotalExecutedCycles => Api.core.snes_get_executed_cycles();
private void StepInto()
{
_framePassed = Api.core.snes_cpu_step();
TotalExecutedCycles++;
if (_framePassed)
{
Frame++;

View File

@ -10,13 +10,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
public bool FrameAdvance(IController controller, bool render, bool renderSound)
{
_controller = controller;
FrameAdvancePre(controller, render, renderSound);
/* if the input poll callback is called, it will set this to false
* this has to be done before we save the per-frame state in deterministic
* mode, because in there, the core actually advances, and might advance
* through the point in time where IsLagFrame gets set to false. makes sense?
*/
IsLagFrame = true;
bool resetSignal = controller.IsPressed("Reset");
@ -31,6 +26,22 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
Api.core.snes_power();
}
// run the core for one frame
Api.core.snes_run(false);
Frame++;
if (IsLagFrame)
{
LagCount++;
}
return true;
}
internal void FrameAdvancePre(IController controller, bool render, bool renderSound)
{
_controller = controller;
var enables = new BsnesApi.LayerEnables
{
BG1_Prio0 = _settings.ShowBG1_0,
@ -52,20 +63,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
Api.core.snes_set_trace_enabled(_tracer.IsEnabled());
Api.core.snes_set_video_enabled(render);
Api.core.snes_set_audio_enabled(renderSound);
// run the core for one frame
Api.core.snes_run();
Frame++;
if (IsLagFrame)
{
LagCount++;
}
return true;
}
public int Frame { get; private set; }
public int Frame { get; set; }
public string SystemId => VSystemID.Raw.SNES;

View File

@ -14,7 +14,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
{
[CoreConstructor(VSystemID.Raw.SGB)]
[CoreConstructor(VSystemID.Raw.SNES)]
public BsnesCore(CoreLoadParameters<SnesSettings, SnesSyncSettings> loadParameters)
public BsnesCore(CoreLoadParameters<SnesSettings, SnesSyncSettings> loadParameters) : this(loadParameters, false) { }
public BsnesCore(CoreLoadParameters<SnesSettings, SnesSyncSettings> loadParameters, bool subframe = false)
{
var ser = new BasicServiceProvider(this);
ServiceProvider = ser;
@ -60,7 +61,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
Api = new BsnesApi(CoreComm.CoreFileProvider.DllPath(), CoreComm, callbacks.AllDelegatesInMemoryOrder());
_controllers = new BsnesControllers(_syncSettings);
_controllers = new BsnesControllers(_syncSettings, subframe);
generate_palette();
BsnesApi.SnesInitData snesInitData = new()
@ -94,7 +95,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
}
_region = Api.core.snes_get_region();
if (_region == BsnesApi.SNES_REGION.NTSC)
{
// taken from bsnes source
@ -135,7 +135,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
public string BoardName => "SGB";
}
private BsnesApi Api { get; }
internal BsnesApi Api { get; }
private string snes_path_request(int slot, string hint, bool required)
{

View File

@ -0,0 +1,113 @@
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.BSNES
{
[PortedCore(CoreNames.SubBsnes115, "")]
[ServiceNotApplicable(new[] { typeof(IDriveLight) })]
public class SubBsnesCore : IEmulator, ICycleTiming
{
[CoreConstructor(VSystemID.Raw.SGB)]
[CoreConstructor(VSystemID.Raw.SNES)]
public SubBsnesCore(CoreLoadParameters<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings> loadParameters)
{
_bsnesCore = new BsnesCore(loadParameters, true);
if (_bsnesCore.Region == DisplayType.NTSC)
ClockRate = 315.0 / 88.0 * 1000000.0 * 6.0;
else
ClockRate = (283.75 * 15625.0 + 25.0) * 4.8;
BasicServiceProvider ser = new(this);
ser.Register(_bsnesCore.ServiceProvider.GetService<IDebuggable>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IVideoProvider>());
ser.Register(_bsnesCore.ServiceProvider.GetService<ISaveRam>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IStatable>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IInputPollable>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IRegionable>());
ser.Register(_bsnesCore.ServiceProvider.GetService<ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings>>());
ser.Register(_bsnesCore.ServiceProvider.GetService<ISoundProvider>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IMemoryDomains>());
ser.Register(_bsnesCore.ServiceProvider.GetService<IDisassemblable>());
ser.Register(_bsnesCore.ServiceProvider.GetService<ITraceable>());
if (IsSGB)
{
// board info is only set in SGB mode
ser.Register(_bsnesCore.ServiceProvider.GetService<IBoardInfo>());
}
ServiceProvider = ser;
}
private readonly BsnesCore _bsnesCore;
public bool IsSGB => _bsnesCore.IsSGB;
public IEmulatorServiceProvider ServiceProvider { get; }
public ControllerDefinition ControllerDefinition => _bsnesCore.ControllerDefinition;
public bool FrameAdvance(IController controller, bool render, bool renderSound = true)
{
_bsnesCore.FrameAdvancePre(controller, render, renderSound);
_bsnesCore.IsLagFrame = true;
bool framePassed = false;
bool resetSignal = controller.IsPressed("Reset");
bool powerSignal = controller.IsPressed("Power");
if (resetSignal || powerSignal)
{
int resetInstruction = controller.AxisValue("Reset Instruction");
for (int i = 0; i < resetInstruction; i++)
{
framePassed = _bsnesCore.Api.core.snes_cpu_step();
if (framePassed) break;
}
if (resetSignal)
{
_bsnesCore.Api.core.snes_reset();
}
if (powerSignal)
{
_bsnesCore.Api.core.snes_power();
}
}
else
{
// run the core for one (sub-)frame
bool subFrameRequested = controller.IsPressed("Subframe");
framePassed = _bsnesCore.Api.core.snes_run(subFrameRequested);
}
_bsnesCore.Frame++;
if (framePassed && _bsnesCore.IsLagFrame)
_bsnesCore.LagCount++;
else
_bsnesCore.IsLagFrame = false;
return true;
}
public int Frame => _bsnesCore.Frame;
public string SystemId => _bsnesCore.SystemId;
public bool DeterministicEmulation => _bsnesCore.DeterministicEmulation;
public void ResetCounters()
{
_bsnesCore.ResetCounters();
}
public void Dispose()
{
_bsnesCore.Dispose();
}
public long CycleCount => _bsnesCore.Api.core.snes_get_executed_cycles();
public double ClockRate { get; }
}
}

View File

@ -50,6 +50,7 @@ namespace BizHawk.Emulation.Cores
public const string Saturnus = "Saturnus";
public const string SMSHawk = "SMSHawk";
public const string Snes9X = "Snes9x";
public const string SubBsnes115 = "SubBSNESv115+";
public const string SubGbHawk = "SubGBHawk";
public const string SubNesHawk = "SubNESHawk";
public const string TI83Hawk = "TI83Hawk";

View File

@ -20,6 +20,7 @@ namespace BizHawk.Emulation.Cores
{
LibsnesCore libsnes => GetLibsnesPadSchemas(libsnes),
BsnesCore bsnes => GetBsnesPadSchemas(bsnes),
SubBsnesCore subBsnes => GetBsnesPadSchemas(subBsnes.ServiceProvider.GetService<ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings>>()),
NymaCore nyma => GetFaustSchemas(nyma, showMessageBox),
_ => GetSnes9xPadSchemas((Snes9x) core)
};
@ -112,9 +113,9 @@ namespace BizHawk.Emulation.Cores
yield return ConsoleButtons();
}
private IEnumerable<PadSchema> GetBsnesPadSchemas(BsnesCore core)
private IEnumerable<PadSchema> GetBsnesPadSchemas(ISettable<BsnesCore.SnesSettings, BsnesCore.SnesSyncSettings> settingsProvider)
{
var syncSettings = core.GetSyncSettings();
var syncSettings = settingsProvider.GetSyncSettings();
var ports = new[]
{

View File

@ -58,6 +58,7 @@ auto SuperMultitap::latch(bool data) -> void {
counter2 = 0;
if(latched == 0) {
if (port == ID::Port::Controller1) platform->notify("LATCH");
uint offset = isPayloadController ? 16 : 12;
for(uint id : range(4)) {
auto& gamepad = gamepads[id];

View File

@ -28,7 +28,10 @@ auto CPU::Enter() -> void {
while(true) {
scheduler.synchronize();
cpu.main();
if (scheduler.StepOnce) scheduler.leave(Scheduler::Event::Desynchronized);
if (scheduler.StepOnce) {
scheduler.StepOnce = false;
scheduler.leave(Scheduler::Event::Desynchronized);
}
}
}

View File

@ -76,6 +76,8 @@ struct CPU : Processor::WDC65816, Thread, PPUcounter {
uint target = 0;
} overclocking;
long TotalExecutedCycles;
private:
uint version = 2; //allowed: 1, 2

View File

@ -10,6 +10,7 @@ auto CPU::joypadCounter() const -> uint {
auto CPU::stepOnce() -> void {
counter.cpu += 2;
TotalExecutedCycles += 2;
tick();
if(hcounter() & 2) nmiPoll(), irqPoll();
if(joypadCounter() == 0) joypadEdge();

View File

@ -169,9 +169,11 @@ EXPORT void snes_reset(void)
}
// note: run with runahead doesn't work yet, i suspect it's due to the serialize thing breaking (cause of libco)
EXPORT void snes_run(void)
EXPORT bool snes_run(bool breakOnLatch)
{
program->breakOnLatch = breakOnLatch;
emulator->run();
return scheduler.event == Scheduler::Event::Frame;
}
// not used, but would probably be nice
@ -449,10 +451,14 @@ EXPORT bool snes_cpu_step()
{
scheduler.StepOnce = true;
emulator->run();
scheduler.StepOnce = false;
return scheduler.event == Scheduler::Event::Frame;
}
EXPORT long snes_get_executed_cycles()
{
return SuperFamicom::cpu.TotalExecutedCycles;
}
// should be called on savestate load, to get msu files loaded and in the correct state
EXPORT void snes_msu_sync()
{

View File

@ -43,6 +43,7 @@ struct Program : Emulator::Platform
bool overscan = false;
uint16_t backdropColor;
int regionOverride = 0;
bool breakOnLatch;
public:
struct Game {
@ -463,8 +464,13 @@ auto Program::notify(string message) -> void
snesCallbacks.snes_no_lag(false);
else if (message == "NO_LAG_SGB")
snesCallbacks.snes_no_lag(true);
else if (message == "LATCH")
else if (message == "LATCH") {
if (breakOnLatch) {
scheduler.StepOnce = true;
breakOnLatch = false;
}
snesCallbacks.snes_controller_latch();
}
}
auto Program::cpuTrace(vector<string> parts) -> void