From 49fe8dabf4a77937c6be3a9067ed3bda7fadf353 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:57:11 +0000 Subject: [PATCH 1/4] feat(gameboy): implement native Game Boy DMG/GBC sound driver, target, and CI workflow - Implemented hardware APU driver in drivers/gb_sound/gb_rdj.c controlling Pulse 1, Pulse 2, Wavetable, and Noise channels. - Implemented real hardware backend in backends/backend_gb_hw.c with direct APU memory-mapped register access and wait_vbl_done(). - Added native 20x18 Game Boy tracker UI and build pipeline under targets/gameboy/ producing out/rom.gb via GBDK-2020 lcc. - Created dedicated CI workflow .github/workflows/build-gameboy.yml with manual workflow_dispatch support. - Integrated Game Boy driver/backend into PC host test runner with zero regressions. Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- .github/workflows/build-gameboy.yml | 57 +++++ .github/workflows/build-genesis.yml | 4 + .gitignore | 8 + backends/backend_gb_hw.c | 105 +++++++++ drivers/gb_sound/gb_rdj.c | 253 +++++++++++++++++++++ targets/gameboy/Makefile | 37 +++ targets/gameboy/src/main.c | 45 ++++ targets/gameboy/src/ui_gb.c | 341 ++++++++++++++++++++++++++++ targets/gameboy/src/ui_gb.h | 33 +++ targets/genesis/src/input_mapper.c | 9 +- 10 files changed, 888 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-gameboy.yml create mode 100644 backends/backend_gb_hw.c create mode 100644 drivers/gb_sound/gb_rdj.c create mode 100644 targets/gameboy/Makefile create mode 100644 targets/gameboy/src/main.c create mode 100644 targets/gameboy/src/ui_gb.c create mode 100644 targets/gameboy/src/ui_gb.h diff --git a/.github/workflows/build-gameboy.yml b/.github/workflows/build-gameboy.yml new file mode 100644 index 0000000..bca3e03 --- /dev/null +++ b/.github/workflows/build-gameboy.yml @@ -0,0 +1,57 @@ +name: Game Boy Build + +on: + push: + branches: [ main ] + paths: + - 'targets/gameboy/**' + - 'backends/**' + - 'core/**' + - 'drivers/gb_sound/**' + - '.github/workflows/build-gameboy.yml' + pull_request: + branches: [ main ] + paths: + - 'targets/gameboy/**' + - 'backends/**' + - 'core/**' + - 'drivers/gb_sound/**' + - '.github/workflows/build-gameboy.yml' + workflow_dispatch: + +jobs: + + build-gameboy: + name: Game Boy ROM (GBDK-2020) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install GBDK-2020 + run: | + wget -q https://github.com/gbdk-2020/gbdk-2020/releases/download/4.5.0/gbdk-linux64.tar.gz + tar -xzf gbdk-linux64.tar.gz + echo "$GITHUB_WORKSPACE/gbdk/bin" >> $GITHUB_PATH + + - name: Build Game Boy ROM + run: | + make -C targets/gameboy/ + + - name: Verify Game Boy ROM Output + run: | + if [ -f targets/gameboy/out/rom.gb ]; then + echo "Successfully generated rom.gb!" + ls -la targets/gameboy/out/rom.gb + else + echo "Error: targets/gameboy/out/rom.gb not found!" + ls -la targets/gameboy/out/ || true + false + fi + + - name: Upload Game Boy ROM + uses: actions/upload-artifact@v4 + with: + name: RDJ-gameboy-rom + path: targets/gameboy/out/rom.gb diff --git a/.github/workflows/build-genesis.yml b/.github/workflows/build-genesis.yml index 04054aa..0f6c7f8 100644 --- a/.github/workflows/build-genesis.yml +++ b/.github/workflows/build-genesis.yml @@ -8,6 +8,7 @@ on: - 'backends/**' - 'core/**' - 'drivers/ym2612/**' + - 'drivers/gb_sound/**' - 'tests/**' - '.github/workflows/build-genesis.yml' pull_request: @@ -17,6 +18,7 @@ on: - 'backends/**' - 'core/**' - 'drivers/ym2612/**' + - 'drivers/gb_sound/**' - 'tests/**' - '.github/workflows/build-genesis.yml' workflow_dispatch: @@ -82,10 +84,12 @@ jobs: tests/test_main.c \ drivers/ym2612/ym2612_rdj.c \ drivers/nes_2a03/2a03_rdj.c \ + drivers/gb_sound/gb_rdj.c \ backends/backend_null.c \ backends/backend_vgm.c \ backends/backend_serial.c \ backends/backend_genesis_hw.c \ + backends/backend_gb_hw.c \ targets/genesis/src/ui.c \ targets/genesis/src/input_mapper.c \ -I. -Icore/ -o test_runner diff --git a/.gitignore b/.gitignore index d331e7f..3877198 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,17 @@ *.s !targets/nes/src/crt0.s out/ +targets/gameboy/out/ test_runner test_output.vgm +# GBDK intermediate build artifacts +*.ihx +*.lst +*.sym +*.noi +*.map + # SGDK copied sources targets/genesis/src/core/ targets/genesis/src/drivers/ diff --git a/backends/backend_gb_hw.c b/backends/backend_gb_hw.c new file mode 100644 index 0000000..ffec784 --- /dev/null +++ b/backends/backend_gb_hw.c @@ -0,0 +1,105 @@ +/* GPLv3 - RetroDJ Project - Game Boy Real Hardware Backend */ + +/* + * Copyright (C) 2025 RetroDJ Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should receive a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#if defined(RDJ_GENESIS_HARDWARE) || defined(SGDK_GCC) || defined(__m68k__) + #include +#elif defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + #include + #include + #include +#else + #include + #include +#endif + +#include "core/rdj_backend.h" + +#if !defined(__PORT_sm83) && !defined(__TARGET_gb) && !defined(GBDK) +static uint8_t gb_apu_shadow[0x40]; +#endif + +static void gb_hw_write_apu(uint16_t addr, uint8_t val) { +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + *((volatile uint8_t*)addr) = val; +#else + if (addr >= 0xFF10 && addr <= 0xFF3F) { + gb_apu_shadow[addr - 0xFF10] = val; + } +#endif +} + +static void gb_hw_init(void) { + /* Master enable APU: Bit 7 = 1 in NR52 ($FF26) */ + gb_hw_write_apu(0xFF26, 0x80); + /* Enable all output channels to left and right terminals in NR51 ($FF25) */ + gb_hw_write_apu(0xFF25, 0xFF); + /* Set max stereo master volume in NR50 ($FF24) */ + gb_hw_write_apu(0xFF24, 0x77); +} + +static void gb_hw_shutdown(void) { + /* Disable APU master enable */ + gb_hw_write_apu(0xFF26, 0x00); +} + +static void gb_hw_write_ym2612(uint8_t port, uint8_t reg, uint8_t val) { + (void)port; + (void)reg; + (void)val; +} + +static void gb_hw_wait_samples(uint16_t samples) { + (void)samples; +} + +static void gb_hw_wait_frame(void) { +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + wait_vbl_done(); +#endif +} + +static const char gb_backend_name[] = "Game Boy Real Hardware Backend"; + +rdj_backend_t backend_gb_hw = { + gb_backend_name, + gb_hw_init, + gb_hw_shutdown, + gb_hw_write_ym2612, + gb_hw_write_apu, + gb_hw_wait_samples, + gb_hw_wait_frame +}; + +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) +rdj_backend_t *rdj_active_backend = &backend_gb_hw; + +void rdj_set_backend(rdj_backend_t *backend) { + if (backend) { + if (rdj_active_backend && rdj_active_backend->shutdown) { + rdj_active_backend->shutdown(); + } + rdj_active_backend = backend; + if (rdj_active_backend->init) { + rdj_active_backend->init(); + } + } else { + rdj_active_backend = &backend_gb_hw; + } +} +#endif diff --git a/drivers/gb_sound/gb_rdj.c b/drivers/gb_sound/gb_rdj.c new file mode 100644 index 0000000..d1d8fe2 --- /dev/null +++ b/drivers/gb_sound/gb_rdj.c @@ -0,0 +1,253 @@ +/* GPLv3 - RetroDJ Project - Game Boy Sound Hardware Driver */ + +/* + * Copyright (C) 2025 RetroDJ Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should receive a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#if defined(RDJ_GENESIS_HARDWARE) || defined(SGDK_GCC) || defined(__m68k__) + #include +#elif defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + #include + #include + #include +#else + #include + #include +#endif + +#include "core/rdj_driver.h" +#include "core/rdj_backend.h" + +/* Register map (Game Boy APU hardware registers $FF10 - $FF26) */ +#define GB_NR10 0xFF10 /* CH1 Sweep */ +#define GB_NR11 0xFF11 /* CH1 Duty / Length */ +#define GB_NR12 0xFF12 /* CH1 Envelope */ +#define GB_NR13 0xFF13 /* CH1 Frequency Low */ +#define GB_NR14 0xFF14 /* CH1 Frequency High / Trigger */ + +#define GB_NR21 0xFF16 /* CH2 Duty / Length */ +#define GB_NR22 0xFF17 /* CH2 Envelope */ +#define GB_NR23 0xFF18 /* CH2 Frequency Low */ +#define GB_NR24 0xFF19 /* CH2 Frequency High / Trigger */ + +#define GB_NR30 0xFF1A /* CH3 Sound On/Off */ +#define GB_NR31 0xFF1B /* CH3 Length */ +#define GB_NR32 0xFF1C /* CH3 Output Level */ +#define GB_NR33 0xFF1D /* CH3 Frequency Low */ +#define GB_NR34 0xFF1E /* CH3 Frequency High / Trigger */ + +#define GB_NR41 0xFF20 /* CH4 Length */ +#define GB_NR42 0xFF21 /* CH4 Envelope */ +#define GB_NR43 0xFF22 /* CH4 Polynomial Counter */ +#define GB_NR44 0xFF23 /* CH4 Control / Trigger */ + +#define GB_NR50 0xFF24 /* Master Volume / VIN */ +#define GB_NR51 0xFF25 /* Panning / Sound Output Terminal */ +#define GB_NR52 0xFF26 /* Audio Master On/Off */ + +#define GB_WAVE_RAM 0xFF30 /* 16 bytes Wave RAM (32 4-bit samples) */ + +typedef void (*apu_write_fn)(uint16_t addr, uint8_t val); + +/* Forward Declarations */ +void gb_sound_rdj_init(uint32_t clock); +void gb_sound_rdj_reset(void); +void gb_sound_rdj_write(uint16_t addr, uint8_t val); +void gb_sound_note_on(uint8_t ch, uint8_t note, uint8_t inst, uint8_t vol); +void gb_sound_note_off(uint8_t ch); + +void gb_sound_rdj_write(uint16_t addr, uint8_t val) { + if (rdj_active_backend) { + apu_write_fn fn = rdj_active_backend->write_apu; + if (fn) { + fn(addr, val); + } + } +} + +/* Precomputed 128-note MIDI frequency timer values for Game Boy: + * raw_freq = 2048 - (131072 / freq_hz) + * Clamped to 11-bit limit (0..2047) + */ +static const uint16_t gb_frequency_table[128] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 44, 157, 263, 363, + 457, 547, 631, 711, 786, 856, 923, 986, + 1046, 1102, 1155, 1205, 1253, 1297, 1339, 1379, + 1417, 1452, 1486, 1517, 1547, 1575, 1602, 1627, + 1650, 1673, 1694, 1714, 1732, 1750, 1767, 1783, + 1798, 1812, 1825, 1837, 1849, 1860, 1871, 1881, + 1890, 1899, 1907, 1915, 1923, 1930, 1936, 1943, + 1949, 1954, 1959, 1964, 1969, 1974, 1978, 1982, + 1985, 1989, 1992, 1995, 1998, 2001, 2004, 2006, + 2009, 2011, 2013, 2015, 2017, 2018, 2020, 2022, + 2023, 2025, 2026, 2027, 2028, 2029, 2030, 2031, + 2032, 2033, 2034, 2035, 2036, 2036, 2037, 2038 +}; + +/* Standard 16-byte (32 4-bit samples) Triangle / Saw RAM pattern */ +static const uint8_t gb_default_wave_ram[16] = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21, 0x00 +}; + +static uint16_t gb_calc_frequency(uint8_t note) { + if (note > 127) note = 127; + return gb_frequency_table[note]; +} + +void gb_sound_rdj_init(uint32_t clock) { + (void)clock; + gb_sound_rdj_reset(); +} + +void gb_sound_rdj_reset(void) { + uint8_t i; + + /* 1. Master enable APU: Bit 7 MUST be set to 1 in NR52 */ + gb_sound_rdj_write(GB_NR52, 0x80); + + /* 2. Master Volume: Left 7, Right 7 */ + gb_sound_rdj_write(GB_NR50, 0x77); + + /* 3. Panning: Enable CH1-CH4 on both Left & Right channels */ + gb_sound_rdj_write(GB_NR51, 0xFF); + + /* 4. Disable sweep on CH1 */ + gb_sound_rdj_write(GB_NR10, 0x00); + + /* 5. Silence CH1 & CH2 envelopes */ + gb_sound_rdj_write(GB_NR12, 0x00); + gb_sound_rdj_write(GB_NR22, 0x00); + + /* 6. Disable CH3 DAC, silence CH3 output level */ + gb_sound_rdj_write(GB_NR30, 0x00); + gb_sound_rdj_write(GB_NR32, 0x00); + + /* 7. Silence CH4 noise envelope */ + gb_sound_rdj_write(GB_NR42, 0x00); + + /* 8. Load default Wave RAM for CH3 */ + gb_sound_rdj_write(GB_NR30, 0x80); /* Enable CH3 DAC to access Wave RAM */ + for (i = 0; i < 16; i++) { + gb_sound_rdj_write(GB_WAVE_RAM + i, gb_default_wave_ram[i]); + } +} + +void gb_sound_note_on(uint8_t ch, uint8_t note, uint8_t inst, uint8_t vol) { + uint16_t freq; + uint8_t vol_4bit; + uint8_t duty_bits; + uint8_t shift_val; + uint8_t noise_poly; + + (void)inst; + vol_4bit = vol ? (vol >> 3) : 15; /* Map 0..128 -> 0..15 range, default 15 */ + if (vol_4bit > 15) vol_4bit = 15; + + if (ch == 0) { + /* CH1: Pulse 1 with Sweep */ + freq = gb_calc_frequency(note); + duty_bits = 0x80; /* 50% duty cycle */ + + gb_sound_rdj_write(GB_NR10, 0x00); /* Disable sweep */ + gb_sound_rdj_write(GB_NR11, duty_bits); /* Duty cycle + 0 length */ + gb_sound_rdj_write(GB_NR12, (vol_4bit << 4)); /* Initial volume, envelope stop */ + gb_sound_rdj_write(GB_NR13, (uint8_t)(freq & 0xFF)); /* Freq low 8 bits */ + gb_sound_rdj_write(GB_NR14, (uint8_t)(0x80 | ((freq >> 8) & 0x07))); /* Trigger (bit 7) + Freq high 3 bits */ + } + else if (ch == 1) { + /* CH2: Pulse 2 */ + freq = gb_calc_frequency(note); + duty_bits = 0x80; /* 50% duty cycle */ + + gb_sound_rdj_write(GB_NR21, duty_bits); + gb_sound_rdj_write(GB_NR22, (vol_4bit << 4)); + gb_sound_rdj_write(GB_NR23, (uint8_t)(freq & 0xFF)); + gb_sound_rdj_write(GB_NR24, (uint8_t)(0x80 | ((freq >> 8) & 0x07))); + } + else if (ch == 2) { + /* CH3: Wavetable */ + freq = gb_calc_frequency(note); + shift_val = 0x20; /* 100% volume level (bits 6-5 = 01 -> 0x20) */ + + if (vol_4bit == 0) { + shift_val = 0x00; /* Mute */ + } else if (vol_4bit < 6) { + shift_val = 0x60; /* 25% */ + } else if (vol_4bit < 11) { + shift_val = 0x40; /* 50% */ + } + + gb_sound_rdj_write(GB_NR30, 0x80); /* Enable CH3 DAC */ + gb_sound_rdj_write(GB_NR32, shift_val); + gb_sound_rdj_write(GB_NR33, (uint8_t)(freq & 0xFF)); + gb_sound_rdj_write(GB_NR34, (uint8_t)(0x80 | ((freq >> 8) & 0x07))); + } + else if (ch == 3) { + /* CH4: Noise */ + noise_poly = (note & 0x0F) | ((note & 0x70) >> 1); /* Period / clock shift */ + + gb_sound_rdj_write(GB_NR41, 0x00); + gb_sound_rdj_write(GB_NR42, (vol_4bit << 4)); + gb_sound_rdj_write(GB_NR43, noise_poly); + gb_sound_rdj_write(GB_NR44, 0x80); /* Trigger */ + } +} + +void gb_sound_note_off(uint8_t ch) { + if (ch == 0) { + gb_sound_rdj_write(GB_NR12, 0x00); + gb_sound_rdj_write(GB_NR14, 0x80); /* Re-trigger silenced envelope */ + } else if (ch == 1) { + gb_sound_rdj_write(GB_NR22, 0x00); + gb_sound_rdj_write(GB_NR24, 0x80); + } else if (ch == 2) { + gb_sound_rdj_write(GB_NR32, 0x00); /* Output level = Mute */ + } else if (ch == 3) { + gb_sound_rdj_write(GB_NR42, 0x00); + gb_sound_rdj_write(GB_NR44, 0x80); + } +} + +static void gb_sound_wrapper_write_reg(uint16_t reg, uint8_t val) { + gb_sound_rdj_write(reg, val); +} + +static void gb_sound_wrapper_shutdown(void) { + gb_sound_rdj_reset(); + gb_sound_rdj_write(GB_NR52, 0x00); /* Disable APU */ +} + +static const char gb_driver_name[] = "Game Boy APU Sound Driver"; +static const char gb_chip_name[] = "DMG/GBC APU"; + +/* Global unified driver structure instance */ +rdj_driver_t gb_sound_driver = { + gb_driver_name, + gb_chip_name, + 4, + 16, + gb_sound_rdj_init, + gb_sound_rdj_reset, + gb_sound_note_on, + gb_sound_note_off, + gb_sound_wrapper_write_reg, + gb_sound_wrapper_shutdown +}; diff --git a/targets/gameboy/Makefile b/targets/gameboy/Makefile new file mode 100644 index 0000000..feee39b --- /dev/null +++ b/targets/gameboy/Makefile @@ -0,0 +1,37 @@ +# RetroDJ - Game Boy Target Makefile (GBDK-2020) +# GPLv3 - RetroDJ Project + +LCC := lcc + +INCS := -I. -Isrc -I../../ -I../../core -I../../drivers/gb_sound -I../../backends -I../../targets/genesis/src +CFLAGS := -Wa-l -Wl-m -Wl-j -DUSE_SFR_FOR_REG -Wm-yc $(INCS) + +OUT_DIR := out +TARGET := $(OUT_DIR)/rom.gb + +C_SRCS := src/main.c \ + src/ui_gb.c \ + ../../drivers/gb_sound/gb_rdj.c \ + ../../backends/backend_gb_hw.c \ + ../../targets/genesis/src/input_mapper.c + +C_OBJS := $(C_SRCS:.c=.o) + +.SUFFIXES: +.SUFFIXES: .c .o + +.PHONY: all clean + +all: $(TARGET) + +%.o: %.c + $(LCC) $(CFLAGS) -c $< -o $@ + +$(TARGET): $(C_OBJS) + mkdir -p $(OUT_DIR) + $(LCC) $(CFLAGS) $(C_OBJS) -o $(TARGET) + +clean: + rm -rf $(OUT_DIR) + rm -f src/*.o ../../drivers/gb_sound/*.o ../../backends/*.o ../../targets/genesis/src/*.o + rm -f src/*.lst src/*.sym ../../drivers/gb_sound/*.lst ../../backends/*.lst diff --git a/targets/gameboy/src/main.c b/targets/gameboy/src/main.c new file mode 100644 index 0000000..780eea5 --- /dev/null +++ b/targets/gameboy/src/main.c @@ -0,0 +1,45 @@ +/* RetroDJ - Game Boy Main Entry Point */ +/* GPLv3 - RetroDJ Project */ + +/* + * Copyright (C) 2025 RetroDJ Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should receive a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui_gb.h" +#include "core/rdj_driver.h" +#include "core/rdj_backend.h" + +extern rdj_driver_t gb_sound_driver; +extern rdj_backend_t backend_gb_hw; + +int main(void) { + /* Initialize and activate Game Boy hardware backend */ + rdj_set_backend(&backend_gb_hw); + + /* Initialize Game Boy sound driver */ + gb_sound_driver.init(0); + + /* Initialize Tracker UI */ + ui_init(); + + while (1) { + wait_vbl_done(); + ui_update(); + ui_render(); + } + + return 0; +} diff --git a/targets/gameboy/src/ui_gb.c b/targets/gameboy/src/ui_gb.c new file mode 100644 index 0000000..abaf4d7 --- /dev/null +++ b/targets/gameboy/src/ui_gb.c @@ -0,0 +1,341 @@ +/* RetroDJ - Game Boy Tracker UI Implementation */ +/* GPLv3 - RetroDJ Project */ + +#include "ui_gb.h" +#include "core/rdj_driver.h" +#include "core/rdj_backend.h" + +#include +#include + +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) +#include +#include +#include +#endif + +static rdj_song_t g_song; +static input_tracker_state_t g_tracker_state; + +static uint8_t s_tick_counter = 0; +static uint8_t s_current_play_row = 0; +static uint8_t s_prev_is_playing = 0; + +static uint8_t s_last_cursor_row = 0; +static uint8_t s_last_cursor_chan = 0; +static uint8_t s_last_cursor_subcol = 0; +static uint8_t s_last_start_row = 0; +static uint8_t s_last_status_bpm = 0; +static uint8_t s_last_status_oct = 0; +static uint8_t s_last_status_ch = 0; +static uint8_t s_last_status_playing = 0; + +static char hex_char(uint8_t val) { + val &= 0x0F; + return (val < 10) ? ('0' + val) : ('A' + (val - 10)); +} + +void get_note_name(uint8_t note, char *out) { + uint8_t note_idx; + int octave; + static const char note_names[12][2] = { + {'C', '-'}, {'C', '#'}, {'D', '-'}, {'D', '#'}, + {'E', '-'}, {'F', '-'}, {'F', '#'}, {'G', '-'}, + {'G', '#'}, {'A', '-'}, {'A', '#'}, {'B', '-'} + }; + + if (note == RDJ_NOTE_EMPTY || note == 0) { + out[0] = '-'; out[1] = '-'; out[2] = '-'; out[3] = '\0'; + return; + } + if (note == RDJ_NOTE_OFF) { + out[0] = 'O'; out[1] = 'F'; out[2] = 'F'; out[3] = '\0'; + return; + } + note_idx = (note - 12) % 12; + octave = (note - 12) / 12; + if (octave < 0) octave = 0; + if (octave > 9) octave = 9; + + out[0] = note_names[note_idx][0]; + out[1] = note_names[note_idx][1]; + out[2] = (char)('0' + octave); + out[3] = '\0'; +} + +static void format_status_str(char *out) { + uint16_t bpm = g_song.bpm; + uint8_t oct = g_tracker_state.octave; + uint8_t ch = g_tracker_state.cursor_channel + 1; + bool playing = g_tracker_state.is_playing; + + /* Fits 20 columns: "BPM:120 [STOP] O:4 C:1" */ + out[0] = 'B'; out[1] = 'P'; out[2] = 'M'; out[3] = ':'; + out[4] = (char)('0' + ((bpm / 100) % 10)); + out[5] = (char)('0' + ((bpm / 10) % 10)); + out[6] = (char)('0' + (bpm % 10)); + out[7] = ' '; out[8] = '['; + if (playing) { + out[9] = 'P'; out[10] = 'L'; out[11] = 'A'; out[12] = 'Y'; + } else { + out[9] = 'S'; out[10] = 'T'; out[11] = 'O'; out[12] = 'P'; + } + out[13] = ']'; out[14] = ' '; + out[15] = 'O'; out[16] = ':'; + out[17] = (char)('0' + (oct % 10)); + out[18] = ' '; + out[19] = (char)('0' + (ch % 10)); + out[20] = '\0'; +} + +static void format_row_str(uint8_t r, char *out) { + bool is_cursor_row = (r == g_tracker_state.cursor_row); + char note_name[4]; + uint8_t c; + int pos = 0; + + /* 20 columns: "00>|C-4*0|C-4 0|..." */ + out[pos++] = hex_char(r >> 4); + out[pos++] = hex_char(r); + out[pos++] = is_cursor_row ? '>' : ' '; + out[pos++] = '|'; + + for (c = 0; c < 4; c++) { + rdj_row_t *cell = &g_song.patterns[0].rows[r][c]; + bool is_curr_cell = is_cursor_row && (g_tracker_state.cursor_channel == c); + char sub_marker = (is_curr_cell) ? ((g_tracker_state.cursor_subcol == RDJ_SUBCOL_INST) ? 'i' : '*') : ' '; + + get_note_name(cell->note, note_name); + + out[pos++] = note_name[0]; + out[pos++] = note_name[1]; + out[pos++] = note_name[2]; + out[pos++] = sub_marker; + } + out[pos] = '\0'; +} + +void ui_init(void) { + int r, c; + + g_song.bpm = 120; + g_song.speed = 6; + g_song.order_length = 1; + g_song.order[0] = 0; + + for (r = 0; r < RDJ_MAX_ROWS; r++) { + for (c = 0; c < RDJ_MAX_CHANNELS; c++) { + g_song.patterns[0].rows[r][c].note = RDJ_NOTE_EMPTY; + g_song.patterns[0].rows[r][c].instrument = 0; + g_song.patterns[0].rows[r][c].volume = 0; + g_song.patterns[0].rows[r][c].fx_cmd = 0; + g_song.patterns[0].rows[r][c].fx_val = 0; + } + } + g_song.patterns[0].length = 32; + g_song.patterns[0].speed = 6; + + input_mapper_init(&g_tracker_state); + + s_tick_counter = 0; + s_current_play_row = 0; + s_prev_is_playing = 0; + + s_last_cursor_row = g_tracker_state.cursor_row; + s_last_cursor_chan = g_tracker_state.cursor_channel; + s_last_cursor_subcol = g_tracker_state.cursor_subcol; + s_last_start_row = 0; + s_last_status_bpm = (uint8_t)g_song.bpm; + s_last_status_oct = g_tracker_state.octave; + s_last_status_ch = g_tracker_state.cursor_channel; + s_last_status_playing = g_tracker_state.is_playing; + +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + font_t min_font; + font_init(); + min_font = font_load(font_ibm); + font_set(min_font); + + /* Clear screen and draw initial layout */ + cls(); + gotoxy(0, 0); + printf("--- RetroDJ GameBoy ---"); + + char init_str[24]; + format_status_str(init_str); + gotoxy(0, 1); + printf("%s", init_str); + + gotoxy(0, 2); + printf("RW |PU1 PU2 WAV NOI "); + gotoxy(0, 3); + printf("---|----------------"); + + for (uint8_t i = 0; i < 12; i++) { + format_row_str(i, init_str); + gotoxy(0, 4 + i); + printf("%s", init_str); + } + + gotoxy(0, 16); + printf("--------------------"); + gotoxy(0, 17); + printf("A+DIR:Note B+DIR:Val"); + + SHOW_BKG; +#endif +} + +static void ui_trigger_row_notes(uint8_t row) { + uint8_t ch; + for (ch = 0; ch < 4; ch++) { + rdj_row_t *r = &g_song.patterns[0].rows[row][ch]; + if (r->note != RDJ_NOTE_EMPTY && r->note != RDJ_NOTE_OFF) { + gb_sound_driver.note_on(ch, r->note, r->instrument, r->volume ? r->volume : 128); + } else if (r->note == RDJ_NOTE_OFF) { + gb_sound_driver.note_off(ch); + } + } +} + +void ui_process_playback(void) { + uint8_t speed, pat_len, ch; + if (g_tracker_state.is_playing) { + if (!s_prev_is_playing) { + s_prev_is_playing = 1; + s_tick_counter = 0; + s_current_play_row = g_tracker_state.cursor_row; + ui_trigger_row_notes(s_current_play_row); + } else { + s_tick_counter++; + speed = g_song.patterns[0].speed ? g_song.patterns[0].speed : (g_song.speed ? g_song.speed : 6); + if (s_tick_counter >= speed) { + s_tick_counter = 0; + pat_len = g_song.patterns[0].length ? g_song.patterns[0].length : RDJ_MAX_ROWS; + s_current_play_row++; + if (s_current_play_row >= pat_len) { + s_current_play_row = 0; + } + g_tracker_state.cursor_row = s_current_play_row; + ui_trigger_row_notes(s_current_play_row); + } + } + } else { + if (s_prev_is_playing) { + s_prev_is_playing = 0; + s_tick_counter = 0; + for (ch = 0; ch < 4; ch++) { + gb_sound_driver.note_off(ch); + } + } + } +} + +static uint16_t gb_read_controller(void) { + uint16_t buttons = 0; +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + uint8_t joy = joypad(); + if (joy & J_A) buttons |= BUTTON_A; + if (joy & J_B) buttons |= BUTTON_B; + if (joy & J_START) buttons |= BUTTON_START; + if (joy & J_UP) buttons |= BUTTON_UP; + if (joy & J_DOWN) buttons |= BUTTON_DOWN; + if (joy & J_LEFT) buttons |= BUTTON_LEFT; + if (joy & J_RIGHT) buttons |= BUTTON_RIGHT; +#endif + return buttons; +} + +void ui_update(void) { + uint16_t joy = gb_read_controller(); + input_mapper_update(&g_tracker_state, &g_song, joy); + ui_process_playback(); +} + +void ui_render(void) { +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + char cur_str[24]; + uint8_t start_row, r, i; + + /* 1. Update status bar if changed */ + if (g_song.bpm != s_last_status_bpm || + g_tracker_state.octave != s_last_status_oct || + g_tracker_state.cursor_channel != s_last_status_ch || + g_tracker_state.is_playing != s_last_status_playing) { + + format_status_str(cur_str); + gotoxy(0, 1); + printf("%s", cur_str); + + s_last_status_bpm = (uint8_t)g_song.bpm; + s_last_status_oct = g_tracker_state.octave; + s_last_status_ch = g_tracker_state.cursor_channel; + s_last_status_playing = g_tracker_state.is_playing; + } + + /* 2. Calculate visible start_row */ + start_row = g_tracker_state.cursor_row >= 6 ? g_tracker_state.cursor_row - 6 : 0; + if (start_row > RDJ_MAX_ROWS - 12) start_row = RDJ_MAX_ROWS - 12; + + /* 3. Handle full grid redrawing if start_row changed */ + if (start_row != s_last_start_row) { + s_last_start_row = start_row; + for (i = 0; i < 12; i++) { + r = start_row + i; + format_row_str(r, cur_str); + gotoxy(0, 4 + i); + printf("%s", cur_str); + } + s_last_cursor_row = g_tracker_state.cursor_row; + s_last_cursor_chan = g_tracker_state.cursor_channel; + s_last_cursor_subcol = g_tracker_state.cursor_subcol; + return; + } + + /* 4. Update old and new cursor rows if cursor moved */ + if (g_tracker_state.cursor_row != s_last_cursor_row) { + /* Render previous row without cursor */ + r = s_last_cursor_row; + if (r >= start_row && r < start_row + 12) { + i = r - start_row; + format_row_str(r, cur_str); + gotoxy(0, 4 + i); + printf("%s", cur_str); + } + + s_last_cursor_row = g_tracker_state.cursor_row; + s_last_cursor_chan = g_tracker_state.cursor_channel; + s_last_cursor_subcol = g_tracker_state.cursor_subcol; + + /* Render new active row with cursor */ + r = g_tracker_state.cursor_row; + if (r >= start_row && r < start_row + 12) { + i = r - start_row; + format_row_str(r, cur_str); + gotoxy(0, 4 + i); + printf("%s", cur_str); + } + return; + } + + /* 5. Update active row if channel/subcol or cell content changed */ + r = g_tracker_state.cursor_row; + if (r >= start_row && r < start_row + 12) { + i = r - start_row; + format_row_str(r, cur_str); + gotoxy(0, 4 + i); + printf("%s", cur_str); + } + + s_last_cursor_chan = g_tracker_state.cursor_channel; + s_last_cursor_subcol = g_tracker_state.cursor_subcol; +#endif +} + +input_tracker_state_t *ui_get_tracker_state(void) { + return &g_tracker_state; +} + +rdj_song_t *ui_get_song(void) { + return &g_song; +} diff --git a/targets/gameboy/src/ui_gb.h b/targets/gameboy/src/ui_gb.h new file mode 100644 index 0000000..76d4af9 --- /dev/null +++ b/targets/gameboy/src/ui_gb.h @@ -0,0 +1,33 @@ +/* RetroDJ - Game Boy UI Header */ +/* GPLv3 - RetroDJ Project */ + +#ifndef RDJ_GB_UI_H +#define RDJ_GB_UI_H + +#if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + #include + #include + #include +#else + #include + #include +#endif + +#include "targets/genesis/src/input_mapper.h" +#include "core/sequencer.h" +#include "core/rdj_driver.h" +#include "core/rdj_backend.h" + +extern rdj_driver_t gb_sound_driver; +extern rdj_backend_t backend_gb_hw; + +void ui_init(void); +void ui_update(void); +void ui_render(void); +void ui_process_playback(void); + +void get_note_name(uint8_t note, char *out); +input_tracker_state_t *ui_get_tracker_state(void); +rdj_song_t *ui_get_song(void); + +#endif diff --git a/targets/genesis/src/input_mapper.c b/targets/genesis/src/input_mapper.c index 532908a..59a4c3e 100644 --- a/targets/genesis/src/input_mapper.c +++ b/targets/genesis/src/input_mapper.c @@ -3,8 +3,10 @@ #include "input_mapper.h" +#if defined(RDJ_GENESIS_HARDWARE) || defined(SGDK_GCC) || defined(__m68k__) extern void ym2612_rdj_note_on(uint8_t channel, uint8_t note, uint8_t instrument); extern void ym2612_rdj_note_off(uint8_t channel); +#endif void input_mapper_init(input_tracker_state_t *state) { if (!state) return; @@ -47,14 +49,13 @@ void input_mapper_update(input_tracker_state_t *state, rdj_song_t *song, uint16_ /* BUTTON_C: Play audition note at current cursor */ if (pressed & BUTTON_C) { - uint8_t audition_note; +#if defined(RDJ_GENESIS_HARDWARE) || defined(SGDK_GCC) || defined(__m68k__) cell = &song->patterns[0].rows[state->cursor_row][state->cursor_channel]; - audition_note = (cell->note != RDJ_NOTE_EMPTY && cell->note != RDJ_NOTE_OFF) ? cell->note : (state->octave * 12 + 12); -#if !defined(__NES__) && !defined(__CC65__) + uint8_t audition_note = (cell->note != RDJ_NOTE_EMPTY && cell->note != RDJ_NOTE_OFF) ? cell->note : (state->octave * 12 + 12); ym2612_rdj_note_on(state->cursor_channel, audition_note, cell->instrument); #endif } else if ((state->last_joy & BUTTON_C) && !(joy_state & BUTTON_C)) { -#if !defined(__NES__) && !defined(__CC65__) +#if defined(RDJ_GENESIS_HARDWARE) || defined(SGDK_GCC) || defined(__m68k__) ym2612_rdj_note_off(state->cursor_channel); #endif } From 0b4283cc5d2ee2d2bfee249bdb07d92a2404ae7c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:05:26 +0000 Subject: [PATCH 2/4] fix(ci): fix GBDKDIR trailing slash resolution in Game Boy Makefile and workflow - Updated targets/gameboy/Makefile to automatically append trailing slash to GBDKDIR when required by lcc. - Updated .github/workflows/build-gameboy.yml to export GBDKDIR with trailing slash in GITHUB_ENV. Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- .github/workflows/build-gameboy.yml | 1 + targets/gameboy/Makefile | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-gameboy.yml b/.github/workflows/build-gameboy.yml index bca3e03..adb9ae9 100644 --- a/.github/workflows/build-gameboy.yml +++ b/.github/workflows/build-gameboy.yml @@ -33,6 +33,7 @@ jobs: run: | wget -q https://github.com/gbdk-2020/gbdk-2020/releases/download/4.5.0/gbdk-linux64.tar.gz tar -xzf gbdk-linux64.tar.gz + echo "GBDKDIR=$GITHUB_WORKSPACE/gbdk/" >> $GITHUB_ENV echo "$GITHUB_WORKSPACE/gbdk/bin" >> $GITHUB_PATH - name: Build Game Boy ROM diff --git a/targets/gameboy/Makefile b/targets/gameboy/Makefile index feee39b..d58bfef 100644 --- a/targets/gameboy/Makefile +++ b/targets/gameboy/Makefile @@ -1,7 +1,12 @@ # RetroDJ - Game Boy Target Makefile (GBDK-2020) # GPLv3 - RetroDJ Project -LCC := lcc +GBDKDIR ?= /opt/gbdk +ifneq ($(lastword $(subst /, ,$(GBDKDIR))),) + GBDKDIR := $(GBDKDIR)/ +endif + +LCC := $(GBDKDIR)bin/lcc INCS := -I. -Isrc -I../../ -I../../core -I../../drivers/gb_sound -I../../backends -I../../targets/genesis/src CFLAGS := -Wa-l -Wl-m -Wl-j -DUSE_SFR_FOR_REG -Wm-yc $(INCS) @@ -25,11 +30,11 @@ C_OBJS := $(C_SRCS:.c=.o) all: $(TARGET) %.o: %.c - $(LCC) $(CFLAGS) -c $< -o $@ + GBDKDIR=$(GBDKDIR) $(LCC) $(CFLAGS) -c $< -o $@ $(TARGET): $(C_OBJS) mkdir -p $(OUT_DIR) - $(LCC) $(CFLAGS) $(C_OBJS) -o $(TARGET) + GBDKDIR=$(GBDKDIR) $(LCC) $(CFLAGS) $(C_OBJS) -o $(TARGET) clean: rm -rf $(OUT_DIR) From e4e5d0f418b0897c723083a5a2eff6152dffa146 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:16:27 +0000 Subject: [PATCH 3/4] feat(gameboy): enable Game Boy Color (GBC) Dual-Mode support & LSDj color palettes - Added -Wm-yc CGB Dual Mode header flag to targets/gameboy/Makefile and output rom.gbc alongside rom.gb. - Implemented runtime CGB hardware check (_cpu == CGB_TYPE) in ui_gb.c to apply vibrant LSDj-inspired background palettes with DMG fallback. - Updated .github/workflows/build-gameboy.yml CI workflow to build, verify, and upload rom.gbc artifacts. Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- .github/workflows/build-gameboy.yml | 12 +++++++----- targets/gameboy/src/ui_gb.c | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-gameboy.yml b/.github/workflows/build-gameboy.yml index adb9ae9..81903e1 100644 --- a/.github/workflows/build-gameboy.yml +++ b/.github/workflows/build-gameboy.yml @@ -42,11 +42,11 @@ jobs: - name: Verify Game Boy ROM Output run: | - if [ -f targets/gameboy/out/rom.gb ]; then - echo "Successfully generated rom.gb!" - ls -la targets/gameboy/out/rom.gb + if [ -f targets/gameboy/out/rom.gb ] && [ -f targets/gameboy/out/rom.gbc ]; then + echo "Successfully generated rom.gb and rom.gbc!" + ls -la targets/gameboy/out/ else - echo "Error: targets/gameboy/out/rom.gb not found!" + echo "Error: ROM output missing!" ls -la targets/gameboy/out/ || true false fi @@ -55,4 +55,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: RDJ-gameboy-rom - path: targets/gameboy/out/rom.gb + path: | + targets/gameboy/out/rom.gb + targets/gameboy/out/rom.gbc diff --git a/targets/gameboy/src/ui_gb.c b/targets/gameboy/src/ui_gb.c index abaf4d7..1c7f5b9 100644 --- a/targets/gameboy/src/ui_gb.c +++ b/targets/gameboy/src/ui_gb.c @@ -10,6 +10,7 @@ #if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) #include +#include #include #include #endif @@ -151,6 +152,20 @@ void ui_init(void) { s_last_status_playing = g_tracker_state.is_playing; #if defined(__PORT_sm83) || defined(__TARGET_gb) || defined(GBDK) + if (_cpu == CGB_TYPE) { + /* LSDj-inspired Color Palette: Light BG, Dark Navy Text, Cyan Highlight, Red Marker */ + const palette_color_t cgb_palette[4] = { + RGB8(245, 245, 250), /* Color 0: Light clean background */ + RGB8(30, 130, 230), /* Color 1: Cyan / Blue highlight */ + RGB8(20, 25, 45), /* Color 2: Dark navy text */ + RGB8(225, 50, 50) /* Color 3: Red cursor/accents */ + }; + set_bkg_palette(0, 1, cgb_palette); + } else { + /* DMG Monochrome fallback (Normal 4 shades: 11 10 01 00) */ + BGP_REG = 0xE4; + } + font_t min_font; font_init(); min_font = font_load(font_ibm); From cc154e692fd7724663d9045cb267af87d88d5253 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:19:35 +0000 Subject: [PATCH 4/4] feat(gameboy): enable Game Boy Color (GBC) Dual-Mode support & LSDj color palettes - Added -Wm-yc CGB Dual Mode header flag to targets/gameboy/Makefile and output rom.gbc alongside rom.gb. - Implemented runtime CGB hardware check (_cpu == CGB_TYPE) in ui_gb.c to apply vibrant LSDj-inspired background palettes with DMG fallback. - Updated .github/workflows/build-gameboy.yml CI workflow to build, verify, and upload rom.gbc artifacts. Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- targets/gameboy/Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/targets/gameboy/Makefile b/targets/gameboy/Makefile index d58bfef..f8abe2f 100644 --- a/targets/gameboy/Makefile +++ b/targets/gameboy/Makefile @@ -12,7 +12,8 @@ INCS := -I. -Isrc -I../../ -I../../core -I../../drivers/gb_sound -I../../backend CFLAGS := -Wa-l -Wl-m -Wl-j -DUSE_SFR_FOR_REG -Wm-yc $(INCS) OUT_DIR := out -TARGET := $(OUT_DIR)/rom.gb +TARGET_GB := $(OUT_DIR)/rom.gb +TARGET_GBC := $(OUT_DIR)/rom.gbc C_SRCS := src/main.c \ src/ui_gb.c \ @@ -27,14 +28,17 @@ C_OBJS := $(C_SRCS:.c=.o) .PHONY: all clean -all: $(TARGET) +all: $(TARGET_GB) $(TARGET_GBC) %.o: %.c GBDKDIR=$(GBDKDIR) $(LCC) $(CFLAGS) -c $< -o $@ -$(TARGET): $(C_OBJS) +$(TARGET_GB): $(C_OBJS) mkdir -p $(OUT_DIR) - GBDKDIR=$(GBDKDIR) $(LCC) $(CFLAGS) $(C_OBJS) -o $(TARGET) + GBDKDIR=$(GBDKDIR) $(LCC) $(CFLAGS) $(C_OBJS) -o $(TARGET_GB) + +$(TARGET_GBC): $(TARGET_GB) + cp $(TARGET_GB) $(TARGET_GBC) clean: rm -rf $(OUT_DIR)