diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibPizza.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibPizza.cs index 7075d63cf2..f7a706b3f2 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibPizza.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/LibPizza.cs @@ -12,7 +12,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy public abstract class LibPizza : LibWaterboxCore { [Flags] - public enum Buttons : int + public enum Buttons : uint { A = 0x01, B = 0x02, @@ -29,7 +29,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy public Buttons Keys; } [BizImport(CC)] - public abstract bool Init(byte[] rom, int romlen); + public abstract bool Init(byte[] rom, int romlen, bool sgb); [BizImport(CC)] public abstract bool IsCGB(); } diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Pizza.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Pizza.cs index d89109fe83..f5d7f84e8a 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Pizza.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Pizza.cs @@ -15,6 +15,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy public class Pizza : WaterboxCore, IGameboyCommon { private LibPizza _pizza; + private readonly bool _sgb; [CoreConstructor("GB")] public Pizza(byte[] rom, CoreComm comm) @@ -22,8 +23,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy { DefaultWidth = 160, DefaultHeight = 144, - MaxWidth = 160, - MaxHeight = 144, + MaxWidth = 256, + MaxHeight = 224, MaxSamples = 1024, SystemId = "GB", DefaultFpsNumerator = TICKSPERSECOND, @@ -42,12 +43,16 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy MmapHeapSizeKB = 32 * 1024 }); - if (!_pizza.Init(rom, rom.Length)) + _sgb = true; + if (!_pizza.Init(rom, rom.Length, _sgb)) { throw new InvalidOperationException("Core rejected the rom!"); } PostInit(); + + if (_sgb) + VsyncNumerator = TICKSPERSECOND_SGB; } /// @@ -65,28 +70,50 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy /// private const int TICKSPERSECOND_SGB = 2147727; + #region Controller + + private static readonly ControllerDefinition _definition; + public override ControllerDefinition ControllerDefinition => _definition; + + static Pizza() + { + _definition = new ControllerDefinition { Name = "Gameboy Controller" }; + for (int i = 0; i < 4; i++) + { + _definition.BoolButtons.AddRange( + new[] { "Up", "Down", "Left", "Right", "A", "B", "Select", "Start" } + .Select(s => $"P{i + 1} {s}")); + } + } private static LibPizza.Buttons GetButtons(IController c) { LibPizza.Buttons b = 0; - if (c.IsPressed("Up")) - b |= LibPizza.Buttons.UP; - if (c.IsPressed("Down")) - b |= LibPizza.Buttons.DOWN; - if (c.IsPressed("Left")) - b |= LibPizza.Buttons.LEFT; - if (c.IsPressed("Right")) - b |= LibPizza.Buttons.RIGHT; - if (c.IsPressed("A")) - b |= LibPizza.Buttons.A; - if (c.IsPressed("B")) - b |= LibPizza.Buttons.B; - if (c.IsPressed("Select")) - b |= LibPizza.Buttons.SELECT; - if (c.IsPressed("Start")) - b |= LibPizza.Buttons.START; + for (int i = 4; i > 0; i--) + { + if (c.IsPressed($"P{i} Up")) + b |= LibPizza.Buttons.UP; + if (c.IsPressed($"P{i} Down")) + b |= LibPizza.Buttons.DOWN; + if (c.IsPressed($"P{i} Left")) + b |= LibPizza.Buttons.LEFT; + if (c.IsPressed($"P{i} Right")) + b |= LibPizza.Buttons.RIGHT; + if (c.IsPressed($"P{i} A")) + b |= LibPizza.Buttons.A; + if (c.IsPressed($"P{i} B")) + b |= LibPizza.Buttons.B; + if (c.IsPressed($"P{i} Select")) + b |= LibPizza.Buttons.SELECT; + if (c.IsPressed($"P{i} Start")) + b |= LibPizza.Buttons.START; + if (i != 1) + b = (LibPizza.Buttons)((uint)b << 8); + } return b; } + #endregion + LibPizza.FrameInfo _tmp; // TODO: clean this up so it's not so hacky protected override LibWaterboxCore.FrameInfo FrameAdvancePrep(IController controller, bool render, bool rendersound) @@ -99,11 +126,12 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy protected override void FrameAdvancePost() { - Console.WriteLine(_tmp.Cycles); + //Console.WriteLine(_tmp.Cycles); _tmp = null; } public bool IsCGBMode() => _pizza.IsCGB(); + public bool IsSGBMode() => _sgb; } } diff --git a/waterbox/pizza/lib/global.c b/waterbox/pizza/lib/global.c index d6a04f80d8..c6f489e230 100644 --- a/waterbox/pizza/lib/global.c +++ b/waterbox/pizza/lib/global.c @@ -24,6 +24,7 @@ char global_cart_name[256]; char global_cgb; // if true, in CGB mode +char global_sgb; // if true, in SGB mode char global_cpu_double_speed; char global_debug; char global_rumble; @@ -34,6 +35,7 @@ void global_init() global_window = 1; global_debug = 0; global_cgb = 0; + global_sgb = 0; global_cpu_double_speed = 0; global_rumble = 0; sprintf(global_cart_name, "NOCARTIRDGE"); diff --git a/waterbox/pizza/lib/global.h b/waterbox/pizza/lib/global.h index 0833156835..6ce10887a9 100644 --- a/waterbox/pizza/lib/global.h +++ b/waterbox/pizza/lib/global.h @@ -23,6 +23,7 @@ extern char global_window; extern char global_debug; extern char global_cgb; +extern char global_sgb; // extern char global_started; extern char global_cpu_double_speed; extern char global_rumble; diff --git a/waterbox/pizza/lib/mmu.c b/waterbox/pizza/lib/mmu.c index 8d10ba281e..9da7d095a2 100644 --- a/waterbox/pizza/lib/mmu.c +++ b/waterbox/pizza/lib/mmu.c @@ -33,6 +33,7 @@ #include #include #include +#include "sgb.h" /* GAMEBOY MEMORY AREAS @@ -249,7 +250,7 @@ uint8_t mmu_read(uint16_t a) /* joypad reading */ case 0xFF00: - return input_get_keys(mmu.memory[a]); + return global_sgb ? sgb_read_ff00(cycles.sampleclock) : input_get_keys(mmu.memory[a]); /* CGB HDMA transfer */ case 0xFF55: @@ -944,6 +945,9 @@ void mmu_write(uint16_t a, uint8_t v) /* TODO - put them all */ switch(a) { + case 0xFF00: + sgb_write_ff00(v, cycles.sampleclock); + break; case 0xFF01: case 0xFF02: serial_write_reg(a, v); diff --git a/waterbox/pizza/lib/sgb.c b/waterbox/pizza/lib/sgb.c new file mode 100644 index 0000000000..c41d66a3f0 --- /dev/null +++ b/waterbox/pizza/lib/sgb.c @@ -0,0 +1,278 @@ +#include "sgb.h" +#include "utils.h" +#include +#include + +typedef struct +{ + // writes to FF00 + uint64_t last_write_time; + uint8_t last_write_value; + + // recv packets + uint8_t read_index; // 0-127, index of the next bit read. if 255, not currently reading + uint8_t packet[16]; // a packet in the process of being formed + + uint8_t command[16 * 7]; // a command in the process of being formed + uint8_t expected_packets; // total number of packets expected for a command + uint8_t next_packet; // index of the next packet to be read + + // joypad reading + uint8_t joypad_index; // index of currently reading joypad + uint8_t num_joypads; // number of currently selected joypads (MLT_REQ) + uint8_t joypad_data[4]; // data for each joypad + uint8_t joypad_has_been_read; // state for advancing joypad_index. extermely weird; logic lifted from VBA and probably wrong + + // palettes + uint32_t palette[8][16]; +} sgb_t; + +static sgb_t sgb; + +static uint32_t makecol(uint16_t c) +{ + return c >> 7 & 0xf8 | c >> 12 & 0x07 | c << 6 & 0xf800 | c << 1 & 0x0700 | c << 19 & 0xf80000 | c << 14 & 0x070000 | 0xff000000; +} + +static void cmd_pal(int a, int b) +{ + if ((sgb.command[0] & 7) == 1) + { + uint32_t c[7]; + for (int i = 0; i < 7; i++) + c[i] = makecol(sgb.command[1 + i] | sgb.command[2 + i] << 8); + sgb.palette[0][0] = c[0]; + sgb.palette[a][1] = c[1]; + sgb.palette[a][2] = c[2]; + sgb.palette[a][3] = c[3]; + sgb.palette[b][1] = c[4]; + sgb.palette[b][2] = c[5]; + sgb.palette[b][3] = c[6]; + } + else + { + utils_log("SGB: cmd_pal bad length"); + } +} + +static void cmd_mlt_req(void) +{ + if ((sgb.command[0] & 7) == 1) + { + switch (sgb.command[1] & 3) + { + case 0: + case 2: + sgb.num_joypads = 1; + sgb.joypad_index = 0; + break; + case 1: + sgb.num_joypads = 2; + sgb.joypad_index = 1; + break; + case 3: + sgb.num_joypads = 4; + sgb.joypad_index = 1; + break; + } + utils_log("SGB: %u joypads", sgb.num_joypads); + } + else + { + utils_log("SGB: cmd_mlt_req bad length"); + } +} + +static void do_command(void) +{ + const int command = sgb.command[0] >> 3; + switch (command) + { + default: + utils_log("SGB: Unknown or unimplemented command %02xh", command); + break; + + case 0x00: // PAL01 + utils_log("SGB: PAL01"); + cmd_pal(0, 1); + break; + case 0x01: // PAL23 + utils_log("SGB: PAL23"); + cmd_pal(2, 3); + break; + case 0x02: // PAL03 + utils_log("SGB: PAL03"); + cmd_pal(0, 3); + break; + case 0x03: // PAL12 + utils_log("SGB: PAL12"); + cmd_pal(1, 2); + break; + + // unknown functions + case 0x0c: // ATRC_EN + utils_log("SGB: ATRC_EN??"); + break; + case 0x0d: // TEST_EN + utils_log("SGB: TEST_EN??"); + break; + case 0x0e: // ICON_EN + utils_log("SGB: ICON_EN??"); + break; + case 0x18: // OBJ_TRN + // no game used this + utils_log("SGB: OBJ_TRN??"); + break; + + // unimplementable functions + case 0x10: // DATA_TRN + // TODO: Is it possible for this to write data to + // memory areas used for the attribute file, etc? + // If so, do games do this? + utils_log("SGB: DATA_TRN!!"); + break; + case 0x12: // JUMP + utils_log("SGB: JUMP!!"); + break; + + // joypad + case 0x11: // MLT_REQ + utils_log("SGB: MLT_REQ"); + cmd_mlt_req(); + break; + } +} + +static void do_packet(void) +{ + memcpy(sgb.command + sgb.next_packet * 16, sgb.packet, sizeof(sgb.packet)); + sgb.next_packet++; + + if (sgb.expected_packets == 0) // not in the middle of a command + sgb.expected_packets = sgb.command[0] & 7; + + if (sgb.expected_packets == 0) // huh? + { + utils_log("SGB: zero packet command"); + sgb.expected_packets = 0; + sgb.next_packet = 0; + } + else if (sgb.next_packet == sgb.expected_packets) + { + do_command(); + sgb.expected_packets = 0; + sgb.next_packet = 0; + } +} + +void sgb_init(void) +{ + memset(&sgb, 0, sizeof(sgb)); + sgb.read_index = 255; + sgb.num_joypads = 1; +} + +void sgb_write_ff00(uint8_t val, uint64_t time) +{ + val &= 0x30; + + utils_log("ZZ: %02x, %llu", val, time); + const int p14_fell = (val & 0x10) < (sgb.last_write_value & 0x10); + const int p15_fell = (val & 0x20) < (sgb.last_write_value & 0x20); + const int p14_rose = (val & 0x10) > (sgb.last_write_value & 0x10); + const int p15_rose = (val & 0x20) > (sgb.last_write_value & 0x20); + + if (val == 0) // reset command processing + { + sgb.read_index = 0; + memset(sgb.packet, 0, sizeof(sgb.packet)); + } + else if (sgb.read_index != 255) // currently reading a packet + { + if (p14_fell || p15_fell) + { + if (sgb.read_index == 128) // end of packet + { + if (p14_fell) + do_packet(); + else + utils_log("SGB: Stop bit not present"); + sgb.read_index = 255; + } + else + { + if (p15_fell) + { + int byte = sgb.read_index >> 3; + int bit = sgb.read_index & 7; + sgb.packet[byte] |= 1 << bit; + } + sgb.read_index++; + } + } + } + else // joypad processing + { + if (val == 0x10) + sgb.joypad_has_been_read |= 2; // reading P15 + if (val == 0x20) + sgb.joypad_has_been_read |= 1; // reading P14 + if (val == 0x30 && (p14_rose || p15_rose)) + { + if (sgb.joypad_has_been_read == 7) + { + sgb.joypad_has_been_read = 0; + sgb.joypad_index++; + sgb.joypad_index &= sgb.num_joypads - 1; + utils_log("SGB: joypad index to %u", sgb.joypad_index); + } + else + { + sgb.joypad_has_been_read &= 3; // the other line must be asserted and a read must happen before joypad_index inc?? + } + } + } + + sgb.last_write_value = val; + sgb.last_write_time = time; +} + +uint8_t sgb_read_ff00(uint64_t time) +{ + uint8_t ret = sgb.last_write_value & 0xf0 | 0xc0; + const int p14 = !(ret & 0x10); + const int p15 = !(ret & 0x20); + const int ji = sgb.joypad_index; + + // TODO: is this "reset" correct? + sgb.joypad_has_been_read |= 4; // read occured + sgb.read_index = 255; + sgb.next_packet = 0; + sgb.expected_packets = 0; + + if (!p14 && !p15) + { + utils_log("SGB: SCAN%u", ji); + // scan id + return ret | (15 - ji); + } + else + { + // get data + const uint8_t j = sgb.joypad_data[ji]; + if (p14) + ret |= j >> 4; + if (p15) + ret |= j & 0x0f; + utils_log("SGB: READ%u %02x", ji, ret ^ 0x0f); + return ret ^ 0x0f; + } +} + +// for each of 4 joypads: +// 7......0 +// DULRSsBA +void sgb_set_controller_data(const uint8_t *buttons) +{ + memcpy(sgb.joypad_data, buttons, sizeof(sgb.joypad_data)); +} diff --git a/waterbox/pizza/lib/sgb.h b/waterbox/pizza/lib/sgb.h new file mode 100644 index 0000000000..5db61497c9 --- /dev/null +++ b/waterbox/pizza/lib/sgb.h @@ -0,0 +1,10 @@ +#pragma once +#include + +void sgb_write_ff00(uint8_t val, uint64_t time); + +uint8_t sgb_read_ff00(uint64_t time); + +void sgb_set_controller_data(const uint8_t* buttons); + +void sgb_init(void); diff --git a/waterbox/pizza/pizza.c b/waterbox/pizza/pizza.c index c72a6cc36a..165c92ee1e 100644 --- a/waterbox/pizza/pizza.c +++ b/waterbox/pizza/pizza.c @@ -37,6 +37,7 @@ #include "utils.h" #include "mmu.h" #include "sound_output.h" +#include "sgb.h" /* proto */ void frame_cb(); @@ -54,7 +55,7 @@ int main(void) { } -EXPORT int Init(const void *rom, int romlen) +EXPORT int Init(const void *rom, int romlen, int sgb) { /* init global variables */ global_init(); @@ -64,6 +65,11 @@ EXPORT int Init(const void *rom, int romlen) if (ret != 0) return 0; // failure + global_sgb = !!sgb; + if (global_sgb && global_cgb) + utils_log("Warn: CGB game in SGB mode"); + if (sgb) + sgb_init(); gameboy_init(); @@ -73,7 +79,7 @@ EXPORT int Init(const void *rom, int romlen) /* set rumble cb */ mmu_set_rumble_cb(&rumble_cb); - sound_output_init(2 * 1024 * 1024, 44100); + sound_output_init(global_sgb ? 2147727 : 2097152, 44100); return 1; } @@ -95,7 +101,10 @@ static uint64_t overflow; EXPORT void FrameAdvance(MyFrameInfo* frame) { - input_set_keys(frame->Keys); + if (global_sgb) + sgb_set_controller_data((uint8_t*)&frame->Keys); + else + input_set_keys(frame->Keys); current_vbuff = frame->VideoBuffer; uint64_t current = cycles.sampleclock;