diff --git a/include/apu.h b/include/apu.h new file mode 100644 index 0000000..697d8d6 --- /dev/null +++ b/include/apu.h @@ -0,0 +1,12 @@ +#ifndef APU_H +#define APU_H + +#include "config.h" + +void apu_init(void); +void apu_cleanup(void); +void apu_tick(void); // CALLED ONCE PER M-CYCLE (4 T-CYCLES) +u8 apu_read(u16 addr); +void apu_write(u16 addr, u8 val); + +#endif diff --git a/include/cart.h b/include/cart.h index 92c1a48..a407dd8 100644 --- a/include/cart.h +++ b/include/cart.h @@ -97,6 +97,9 @@ bool load_bootrom(const char* bootrom); bool save_battery(void); bool load_battery(void); +// FREES ROM/RAM AND PERSISTS BATTERY-BACKED SAVE +void cart_cleanup(void); + // GETTERS/SETTERS // get_cart_mode(void); diff --git a/include/config.h b/include/config.h index a91e289..4dd7173 100644 --- a/include/config.h +++ b/include/config.h @@ -6,13 +6,16 @@ #include #include #include +#include // CORE TYPES -typedef uint8_t u8; +typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; -typedef int8_t i8; +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; // EMULATOR SETTINGS #define EMU_TITLE "GBEmu" @@ -22,4 +25,33 @@ typedef int8_t i8; #define EMU_WIDTH (SCREEN_WIDTH * WINDOW_MULTI) #define EMU_HEIGHT (SCREEN_HEIGHT * WINDOW_MULTI) +// LOGGING +#define LOG_TO_FILE 0 // 1 = MIRROR LOGS INTO logs/.log +#define LOG_TO_STDOUT 0 // 1 = ECHO LOGS TO STDOUT +// !VERY EXPENSIVE, FOR TEST-LOG DIFFING ONLY! +#define LOG_VERBOSE_CPU 0 + +// DEBUG VIEWS +#define DEBUG_WINDOW 0 // 1 = OPEN SECOND SDL WINDOW WITH TILE ATLAS + +// INPUT BINDINGS +// NOTE: EDIT BINDS HERE, USE SDLK_UNKNOWN TO LEAVE SLOT UNASSIGNED +#define KEY_GB_RIGHT_PRI SDLK_RIGHT +#define KEY_GB_RIGHT_ALT SDLK_d +#define KEY_GB_LEFT_PRI SDLK_LEFT +#define KEY_GB_LEFT_ALT SDLK_a +#define KEY_GB_UP_PRI SDLK_UP +#define KEY_GB_UP_ALT SDLK_w +#define KEY_GB_DOWN_PRI SDLK_DOWN +#define KEY_GB_DOWN_ALT SDLK_s +#define KEY_GB_A_PRI SDLK_z +#define KEY_GB_A_ALT SDLK_j +#define KEY_GB_B_PRI SDLK_x +#define KEY_GB_B_ALT SDLK_k +#define KEY_GB_SELECT_PRI SDLK_RSHIFT +#define KEY_GB_SELECT_ALT SDLK_BACKSPACE +#define KEY_GB_START_PRI SDLK_RETURN +#define KEY_GB_START_ALT SDLK_UNKNOWN +#define KEY_GB_QUIT SDLK_ESCAPE + #endif diff --git a/include/graphics.h b/include/graphics.h index 0e44f3b..974f4e7 100644 --- a/include/graphics.h +++ b/include/graphics.h @@ -7,9 +7,6 @@ #include "gameboy.h" -// SCREEN CONSTANTS -#define SCREEN_WIDTH 160 -#define SCREEN_HEIGHT 144 #define VRAM_BANK_SIZE 0x2000 #define OAM_SIZE 0xA0 #define TILES_PER_LINE 32 @@ -43,9 +40,9 @@ typedef struct { bool auto_increment; // PALETTE AUTO INCREMENT } cgb_graphics; -typedef struct { +typedef struct __attribute__((packed)) { u8 y; - u8 x; + u8 x; u8 tile; u8 flags; } sprite; @@ -85,6 +82,9 @@ typedef struct { u32 frame_buffer[SCREEN_WIDTH * SCREEN_HEIGHT]; u32 bg_colors[4]; u32 sprite_colors[2][4]; + + // PARALLEL BG INDEX BUFFER (LAYERING SPRITES) + u8 bg_index_line[SCREEN_WIDTH]; // SDL SDL_Window* window; diff --git a/include/interrupt.h b/include/interrupt.h index 4fd8a11..50371a4 100644 --- a/include/interrupt.h +++ b/include/interrupt.h @@ -14,7 +14,7 @@ typedef enum { void interrupt_init(); void interrupt_req(interrupts i); -void handle_interrupts(); +u8 handle_interrupts(); // RETURNS T-CYCLES CONSUMED BY ISR DISPATCH (0 OR 20) u8 get_interrupt_flags(); void set_interrupt_flags(u8 flags); u8 get_interrupt_enable(); diff --git a/include/joypad.h b/include/joypad.h new file mode 100644 index 0000000..041f2c9 --- /dev/null +++ b/include/joypad.h @@ -0,0 +1,26 @@ +#ifndef JOYPAD_H +#define JOYPAD_H + +#include "config.h" + +// LOGICAL BUTTON BITS +typedef enum { + BTN_RIGHT = 0x01, + BTN_LEFT = 0x02, + BTN_UP = 0x04, + BTN_DOWN = 0x08, + BTN_A = 0x10, + BTN_B = 0x20, + BTN_SELECT = 0x40, + BTN_START = 0x80, +} gb_button; + +void joypad_init(void); +void joypad_press(gb_button b); +void joypad_release(gb_button b); + +// P1 REGISTER (FF00) I/O FROM THE BUS +u8 joypad_read(void); +void joypad_write(u8 val); + +#endif diff --git a/include/logger.h b/include/logger.h index 8bf62ee..a4a3cdd 100644 --- a/include/logger.h +++ b/include/logger.h @@ -2,6 +2,7 @@ #define LOGGER_H #include +#include "config.h" // EACH GETS ITS OWN FILE, FIRST ARG OF LOG MACROS IS ONE OF THESE typedef enum { @@ -32,12 +33,24 @@ void logger_init(LogLevel global_level); void logger_cleanup(void); // LOGGING MACROS - USE THESE +#if LOG_TO_FILE || LOG_TO_STDOUT #define LOG_ERROR(component, ...) log_message(component, LOG_ERROR, __VA_ARGS__) #define LOG_WARN(component, ...) log_message(component, LOG_WARN, __VA_ARGS__) #define LOG_INFO(component, ...) log_message(component, LOG_INFO, __VA_ARGS__) #define LOG_DEBUG(component, ...) log_message(component, LOG_DEBUG, __VA_ARGS__) #define LOG_TRACE(component, ...) log_message(component, LOG_TRACE, __VA_ARGS__) +#else +#define LOG_ERROR(component, ...) ((void)0) +#define LOG_WARN(component, ...) ((void)0) +#define LOG_INFO(component, ...) ((void)0) +#define LOG_DEBUG(component, ...) ((void)0) +#define LOG_TRACE(component, ...) ((void)0) +#endif +#if LOG_VERBOSE_CPU #define LOG_TEST(...) log_message(LOG_TESTS, LOG_TEST, __VA_ARGS__) +#else +#define LOG_TEST(...) ((void)0) +#endif // DONT USE THIS void log_message(LogComponent component, LogLevel level, const char* fmt, ...); diff --git a/src/apu.c b/src/apu.c new file mode 100644 index 0000000..df32c4a --- /dev/null +++ b/src/apu.c @@ -0,0 +1,592 @@ +#include "logger.h" +#include "apu.h" +#include "bus.h" +#include +#include + +// SAMPLE RATE WE PRESENT TO SDL. ANYTHING > 22050 IS FINE, 48000 IS COMMON +#define APU_SAMPLE_RATE 48000 + +// CPU TICKS AT ~4.194304 MHZ. ONE SAMPLE EVERY N CPU T-CYCLES: +// ~4194304 / 48000 ~= 87.38 +// WE TRACK A FRACTIONAL ACCUMULATOR TO AVOID DRIFT. +#define APU_CPU_HZ 4194304 +#define SAMPLE_PERIOD_T (APU_CPU_HZ / APU_SAMPLE_RATE) // 87 +#define SAMPLE_PERIOD_REMAINDER (APU_CPU_HZ % APU_SAMPLE_RATE) + +// FRAME SEQUENCER STEPS AT 512 HZ -> EVERY 8192 T-CYCLES +#define FRAME_SEQ_PERIOD_T 8192 + +// AUDIO QUEUE SIZE TARGET (KEEP A SMALL BUFFER TO AVOID UNDERRUNS). +#define AUDIO_QUEUE_TARGET_BYTES (4 * 1024) +#define AUDIO_QUEUE_MAX_BYTES (16 * 1024) + +// SAMPLE BUFFER WE FILL PER FRAME-ISH AND QUEUE TO SDL +#define BATCH_SAMPLES 512 +static i16 sample_buf[BATCH_SAMPLES * 2]; // STEREO +static size_t sample_buf_pos = 0; + +// SDL AUDIO STATE +static SDL_AudioDeviceID audio_dev = 0; + +// DUTY PATTERNS FOR SQUARE CHANNELS (8-STEP WAVEFORMS, 1=HIGH) +static const u8 DUTY_PATTERNS[4] = { + 0x01, // 12.5% 0 0 0 0 0 0 0 1 + 0x81, // 25% 1 0 0 0 0 0 0 1 + 0x87, // 50% 1 0 0 0 0 1 1 1 + 0x7E // 75% 0 1 1 1 1 1 1 0 +}; + +// NOISE DIVISOR TABLE (FOR NR43 LOWER NIBBLE) +static const u16 NOISE_DIVISORS[8] = {8, 16, 32, 48, 64, 80, 96, 112}; + +typedef struct { + // REGISTERS + u8 nr10, nr11, nr12, nr13, nr14; // CH1 + u8 nr21, nr22, nr23, nr24; // CH2 + u8 nr30, nr31, nr32, nr33, nr34; // CH3 + u8 nr41, nr42, nr43, nr44; // CH4 + u8 nr50, nr51, nr52; + u8 wave_ram[16]; + + // POWER + bool power; + + // FRAME SEQUENCER + u32 frame_seq_counter; // T-CYCLES TO NEXT STEP + u8 frame_seq_step; // 0..7 + + // SAMPLE TIMING + u32 sample_acc; // T-CYCLES ACCUMULATED TOWARD NEXT SAMPLE + u32 sample_remainder; // FRACTIONAL REMAINDER ACCUMULATOR + + // CHANNEL 1 (SQUARE WITH SWEEP) + bool ch1_enabled; + u16 ch1_freq_timer; + u8 ch1_duty_pos; + u8 ch1_volume; + u8 ch1_env_timer; + u8 ch1_env_period; + bool ch1_env_up; + u16 ch1_length; // 0..64 + bool ch1_length_enable; + // SWEEP + bool ch1_sweep_enable; + u8 ch1_sweep_timer; + u8 ch1_sweep_period; + bool ch1_sweep_negate; + u8 ch1_sweep_shift; + u16 ch1_shadow_freq; + + // CHANNEL 2 (SQUARE) + bool ch2_enabled; + u16 ch2_freq_timer; + u8 ch2_duty_pos; + u8 ch2_volume; + u8 ch2_env_timer; + u8 ch2_env_period; + bool ch2_env_up; + u16 ch2_length; + bool ch2_length_enable; + + // CHANNEL 3 (WAVE) + bool ch3_enabled; + u16 ch3_freq_timer; + u8 ch3_sample_pos; // 0..31 (32 4-BIT SAMPLES IN WAVE RAM) + u16 ch3_length; // 0..256 + bool ch3_length_enable; + + // CHANNEL 4 (NOISE) + bool ch4_enabled; + u16 ch4_freq_timer; + u16 ch4_lfsr; // 15-BIT LFSR + u8 ch4_volume; + u8 ch4_env_timer; + u8 ch4_env_period; + bool ch4_env_up; + u16 ch4_length; // 0..64 + bool ch4_length_enable; +} apu_state; + +static apu_state apu; +static u16 ch1_frequency(void) { return ((apu.nr14 & 0x07) << 8) | apu.nr13; } +static u16 ch2_frequency(void) { return ((apu.nr24 & 0x07) << 8) | apu.nr23; } +static u16 ch3_frequency(void) { return ((apu.nr34 & 0x07) << 8) | apu.nr33; } + +static void ch1_set_frequency(u16 f) { + apu.nr13 = f & 0xFF; + apu.nr14 = (apu.nr14 & 0xF8) | ((f >> 8) & 0x07); +} + +// SQUARE PERIOD - (2048 - F) * 4 T-CYCLES +static u16 square_period(u16 f) { return (2048 - f) * 4; } +// WAVE PERIOD - (2048 - F) * 2 T-CYCLES +static u16 wave_period(u16 f) { return (2048 - f) * 2; } + +static void trigger_ch1(void) { + apu.ch1_enabled = true; + if (apu.ch1_length == 0) apu.ch1_length = 64; + apu.ch1_freq_timer = square_period(ch1_frequency()); + apu.ch1_env_timer = apu.ch1_env_period; + apu.ch1_volume = (apu.nr12 >> 4) & 0x0F; + apu.ch1_env_up = (apu.nr12 & 0x08) != 0; + + // SWEEP + apu.ch1_shadow_freq = ch1_frequency(); + apu.ch1_sweep_period = (apu.nr10 >> 4) & 0x07; + apu.ch1_sweep_negate = (apu.nr10 & 0x08) != 0; + apu.ch1_sweep_shift = apu.nr10 & 0x07; + apu.ch1_sweep_timer = apu.ch1_sweep_period ? apu.ch1_sweep_period : 8; + apu.ch1_sweep_enable = (apu.ch1_sweep_period != 0) || (apu.ch1_sweep_shift != 0); + + // DAC OFF (UPPER 5 BITS OF NR12 ZERO) DISABLES CHANNEL + if ((apu.nr12 & 0xF8) == 0) apu.ch1_enabled = false; +} + +static void trigger_ch2(void) { + apu.ch2_enabled = true; + if (apu.ch2_length == 0) apu.ch2_length = 64; + apu.ch2_freq_timer = square_period(ch2_frequency()); + apu.ch2_env_timer = apu.ch2_env_period; + apu.ch2_volume = (apu.nr22 >> 4) & 0x0F; + apu.ch2_env_up = (apu.nr22 & 0x08) != 0; + if ((apu.nr22 & 0xF8) == 0) apu.ch2_enabled = false; +} + +static void trigger_ch3(void) { + apu.ch3_enabled = true; + if (apu.ch3_length == 0) apu.ch3_length = 256; + apu.ch3_freq_timer = wave_period(ch3_frequency()); + apu.ch3_sample_pos = 0; + if (!(apu.nr30 & 0x80)) apu.ch3_enabled = false; // DAC OFF +} + +static void trigger_ch4(void) { + apu.ch4_enabled = true; + if (apu.ch4_length == 0) apu.ch4_length = 64; + apu.ch4_lfsr = 0x7FFF; + apu.ch4_env_timer = apu.ch4_env_period; + apu.ch4_volume = (apu.nr42 >> 4) & 0x0F; + apu.ch4_env_up = (apu.nr42 & 0x08) != 0; + if ((apu.nr42 & 0xF8) == 0) apu.ch4_enabled = false; +} + +static void clock_length(void) { + if (apu.ch1_length_enable && apu.ch1_length > 0) { + if (--apu.ch1_length == 0) apu.ch1_enabled = false; + } + if (apu.ch2_length_enable && apu.ch2_length > 0) { + if (--apu.ch2_length == 0) apu.ch2_enabled = false; + } + if (apu.ch3_length_enable && apu.ch3_length > 0) { + if (--apu.ch3_length == 0) apu.ch3_enabled = false; + } + if (apu.ch4_length_enable && apu.ch4_length > 0) { + if (--apu.ch4_length == 0) apu.ch4_enabled = false; + } +} + +static u16 sweep_calc(void) { + u16 next = apu.ch1_shadow_freq >> apu.ch1_sweep_shift; + if (apu.ch1_sweep_negate) { + next = apu.ch1_shadow_freq - next; + } else { + next = apu.ch1_shadow_freq + next; + } + if (next > 2047) { + apu.ch1_enabled = false; + apu.ch1_sweep_enable = false; + } + return next; +} + +static void clock_sweep(void) { + if (!apu.ch1_sweep_enable) return; + if (apu.ch1_sweep_timer > 0) apu.ch1_sweep_timer--; + if (apu.ch1_sweep_timer == 0) { + apu.ch1_sweep_timer = apu.ch1_sweep_period ? apu.ch1_sweep_period : 8; + if (apu.ch1_sweep_period != 0) { + u16 next = sweep_calc(); + if (next <= 2047 && apu.ch1_sweep_shift != 0) { + apu.ch1_shadow_freq = next; + ch1_set_frequency(next); + sweep_calc(); // OVERFLOW CHECK AGAIN + } + } + } +} + +static void clock_envelope_for(u8* timer, u8 period, u8* volume, bool up) { + if (period == 0) return; + if (*timer > 0) (*timer)--; + if (*timer == 0) { + *timer = period; + if (up && *volume < 15) (*volume)++; + else if (!up && *volume > 0) (*volume)--; + } +} + +static void clock_envelopes(void) { + clock_envelope_for(&apu.ch1_env_timer, apu.ch1_env_period, &apu.ch1_volume, apu.ch1_env_up); + clock_envelope_for(&apu.ch2_env_timer, apu.ch2_env_period, &apu.ch2_volume, apu.ch2_env_up); + clock_envelope_for(&apu.ch4_env_timer, apu.ch4_env_period, &apu.ch4_volume, apu.ch4_env_up); +} + +static void frame_sequencer_step(void) { + // STANDARD GB FRAME SEQUENCER PATTERN: + // STEP 0: LENGTH + // STEP 2: LENGTH + SWEEP + // STEP 4: LENGTH + // STEP 6: LENGTH + SWEEP + // STEP 7: ENVELOPE + switch (apu.frame_seq_step) { + case 0: clock_length(); break; + case 2: clock_length(); clock_sweep(); break; + case 4: clock_length(); break; + case 6: clock_length(); clock_sweep(); break; + case 7: clock_envelopes(); break; + default: break; + } + apu.frame_seq_step = (apu.frame_seq_step + 1) & 0x07; +} + +static void step_channels_t(void) { + // CH1 + if (apu.ch1_freq_timer > 0) apu.ch1_freq_timer--; + if (apu.ch1_freq_timer == 0) { + apu.ch1_freq_timer = square_period(ch1_frequency()); + apu.ch1_duty_pos = (apu.ch1_duty_pos + 1) & 0x07; + } + // CH2 + if (apu.ch2_freq_timer > 0) apu.ch2_freq_timer--; + if (apu.ch2_freq_timer == 0) { + apu.ch2_freq_timer = square_period(ch2_frequency()); + apu.ch2_duty_pos = (apu.ch2_duty_pos + 1) & 0x07; + } + // CH3 + if (apu.ch3_freq_timer > 0) apu.ch3_freq_timer--; + if (apu.ch3_freq_timer == 0) { + apu.ch3_freq_timer = wave_period(ch3_frequency()); + apu.ch3_sample_pos = (apu.ch3_sample_pos + 1) & 0x1F; + } + // CH4 + if (apu.ch4_freq_timer > 0) apu.ch4_freq_timer--; + if (apu.ch4_freq_timer == 0) { + u16 divisor = NOISE_DIVISORS[apu.nr43 & 0x07]; + u8 shift = (apu.nr43 >> 4) & 0x0F; + apu.ch4_freq_timer = divisor << shift; + + // LFSR STEP: NEW BIT = OLD BIT0 XOR OLD BIT1 + u16 b = (apu.ch4_lfsr ^ (apu.ch4_lfsr >> 1)) & 1; + apu.ch4_lfsr = (apu.ch4_lfsr >> 1) | (b << 14); + if (apu.nr43 & 0x08) { + apu.ch4_lfsr = (apu.ch4_lfsr & ~(1 << 6)) | (b << 6); + } + } +} + +static u8 ch1_sample(void) { + if (!apu.ch1_enabled) return 0; + u8 pattern = DUTY_PATTERNS[(apu.nr11 >> 6) & 0x03]; + u8 bit = (pattern >> apu.ch1_duty_pos) & 1; + return bit ? apu.ch1_volume : 0; +} + +static u8 ch2_sample(void) { + if (!apu.ch2_enabled) return 0; + u8 pattern = DUTY_PATTERNS[(apu.nr21 >> 6) & 0x03]; + u8 bit = (pattern >> apu.ch2_duty_pos) & 1; + return bit ? apu.ch2_volume : 0; +} + +static u8 ch3_sample(void) { + if (!apu.ch3_enabled || !(apu.nr30 & 0x80)) return 0; + u8 byte = apu.wave_ram[apu.ch3_sample_pos >> 1]; + u8 nibble = (apu.ch3_sample_pos & 1) ? (byte & 0x0F) : (byte >> 4); + u8 shift_code = (apu.nr32 >> 5) & 0x03; + // CODE: 0=MUTE, 1=100%, 2=50%, 3=25% + switch (shift_code) { + case 0: return 0; + case 1: return nibble; + case 2: return nibble >> 1; + case 3: return nibble >> 2; + } + return 0; +} + +static u8 ch4_sample(void) { + if (!apu.ch4_enabled) return 0; + // CHANNEL EMITS WHEN BIT0 OF LFSR IS 0 + if ((apu.ch4_lfsr & 1) == 0) return apu.ch4_volume; + return 0; +} + +// ONE-POLE DC-BLOCKING HPF STATE. ALPHA ~ 1 - 2*PI*FC/FS. +// AT FS=48000 AND FC=78 HZ (DMG ANALOG STAGE), ALPHA ~ 0.9898. +// USING FLOATS HERE; AT ~48K SAMPLES/SEC THIS IS NEGLIGIBLE. +static float hpf_prev_in_l = 0.0f, hpf_prev_out_l = 0.0f; +static float hpf_prev_in_r = 0.0f, hpf_prev_out_r = 0.0f; +static const float HPF_ALPHA = 0.9898f; + +static i16 hpf_step(float in, float* prev_in, float* prev_out) { + float out = HPF_ALPHA * (*prev_out + in - *prev_in); + *prev_in = in; + *prev_out = out; + if (out > 32767.0f) out = 32767.0f; + if (out < -32768.0f) out = -32768.0f; + return (i16)out; +} + +static void emit_sample(void) { + // GATHER 4-BIT CHANNEL OUTPUTS (0..15). CHANNELS THAT ARE INACTIVE + // EMIT 0; ACTIVE ONES EMIT 0..15. + u8 s1 = ch1_sample(); + u8 s2 = ch2_sample(); + u8 s3 = ch3_sample(); + u8 s4 = ch4_sample(); + + // ROUTE TO L/R PER NR51 + u8 nr51 = apu.nr51; + i32 left = 0, right = 0; + if (nr51 & 0x10) left += s1; + if (nr51 & 0x20) left += s2; + if (nr51 & 0x40) left += s3; + if (nr51 & 0x80) left += s4; + if (nr51 & 0x01) right += s1; + if (nr51 & 0x02) right += s2; + if (nr51 & 0x04) right += s3; + if (nr51 & 0x08) right += s4; + + // MASTER VOLS (NR50 3-BIT, HARDWARE = VALUE+1, RANGE 1..8) + u8 lvol = ((apu.nr50 >> 4) & 0x07) + 1; + u8 rvol = (apu.nr50 & 0x07) + 1; + left *= lvol; + right *= rvol; + + // SCALE TO 16-BIT RANGE. PEAK = 4 CHANNELS * 15 * 8 MASTER = 480; + // *60 -> ~28800 PEAK, LEAVES SOME HEADROOM. + float l_in = (float)(left * 60); + float r_in = (float)(right * 60); + + // DC BLOCK + i16 l = hpf_step(l_in, &hpf_prev_in_l, &hpf_prev_out_l); + i16 r = hpf_step(r_in, &hpf_prev_in_r, &hpf_prev_out_r); + + sample_buf[sample_buf_pos++] = l; + sample_buf[sample_buf_pos++] = r; + + if (sample_buf_pos >= BATCH_SAMPLES * 2) { + if (audio_dev != 0) { + Uint32 queued = SDL_GetQueuedAudioSize(audio_dev); + if (queued < AUDIO_QUEUE_MAX_BYTES) { + SDL_QueueAudio(audio_dev, sample_buf, sample_buf_pos * sizeof(i16)); + } + } + sample_buf_pos = 0; + } +} + +void apu_init(void) { + memset(&apu, 0, sizeof(apu)); + apu.power = true; + apu.nr52 = 0xF1; // POWER ON, ALL CHANNELS OFF + apu.nr50 = 0x77; // FULL MASTER VOL ON BOTH SIDES (POST-BOOT VALUE) + apu.nr51 = 0xF3; // POST-BOOT ROUTING + apu.frame_seq_counter = FRAME_SEQ_PERIOD_T; + apu.sample_acc = 0; + apu.ch4_lfsr = 0x7FFF; + + SDL_AudioSpec want = {0}, have = {0}; + want.freq = APU_SAMPLE_RATE; + want.format = AUDIO_S16SYS; + want.channels = 2; + want.samples = 1024; + want.callback = NULL; + + audio_dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + if (audio_dev == 0) { + LOG_WARN(LOG_MAIN, "APU: SDL_OpenAudioDevice failed: %s", SDL_GetError()); + return; + } + SDL_PauseAudioDevice(audio_dev, 0); // UNPAUSE + LOG_INFO(LOG_MAIN, "APU: audio device opened @ %d Hz, %d channels", have.freq, have.channels); +} + +void apu_cleanup(void) { + if (audio_dev != 0) { + SDL_CloseAudioDevice(audio_dev); + audio_dev = 0; + } +} + +void apu_tick(void) { + // CALLED PER M-CYCLE = 4 T-CYCLES + for (int t = 0; t < 4; t++) { + if (apu.power) { + step_channels_t(); + } + + // FRAME SEQUENCER + if (apu.frame_seq_counter > 0) apu.frame_seq_counter--; + if (apu.frame_seq_counter == 0) { + apu.frame_seq_counter = FRAME_SEQ_PERIOD_T; + if (apu.power) frame_sequencer_step(); + } + + // SAMPLE OUTPUT + apu.sample_acc++; + if (apu.sample_acc >= SAMPLE_PERIOD_T) { + apu.sample_acc = 0; + emit_sample(); + } + } +} + +// REGISTER ACCESS +u8 apu_read(u16 addr) { + // WAVE RAM IS READABLE EVEN WHEN POWERED OFF + if (addr >= WAVE_RAM_START && addr <= WAVE_RAM_START + 0xF) { + return apu.wave_ram[addr - WAVE_RAM_START]; + } + + switch (addr) { + // OR-MASKS BELOW REFLECT WHICH BITS READ AS 1 ON REAL HARDWARE + case NR10_REG: return apu.nr10 | 0x80; + case NR11_REG: return apu.nr11 | 0x3F; + case NR12_REG: return apu.nr12; + case NR13_REG: return 0xFF; + case NR14_REG: return apu.nr14 | 0xBF; + + case NR21_REG: return apu.nr21 | 0x3F; + case NR22_REG: return apu.nr22; + case NR23_REG: return 0xFF; + case NR24_REG: return apu.nr24 | 0xBF; + + case NR30_REG: return apu.nr30 | 0x7F; + case NR31_REG: return 0xFF; + case NR32_REG: return apu.nr32 | 0x9F; + case NR33_REG: return 0xFF; + case NR34_REG: return apu.nr34 | 0xBF; + + case NR41_REG: return 0xFF; + case NR42_REG: return apu.nr42; + case NR43_REG: return apu.nr43; + case NR44_REG: return apu.nr44 | 0xBF; + + case NR50_REG: return apu.nr50; + case NR51_REG: return apu.nr51; + case NR52_REG: { + u8 v = 0x70; + if (apu.power) v |= 0x80; + if (apu.ch1_enabled) v |= 0x01; + if (apu.ch2_enabled) v |= 0x02; + if (apu.ch3_enabled) v |= 0x04; + if (apu.ch4_enabled) v |= 0x08; + return v; + } + } + return 0xFF; +} + +void apu_write(u16 addr, u8 val) { + // WAVE RAM IS ALWAYS WRITABLE + if (addr >= WAVE_RAM_START && addr <= WAVE_RAM_START + 0xF) { + apu.wave_ram[addr - WAVE_RAM_START] = val; + return; + } + + // NR52 POWER CONTROL IS ALWAYS WRITABLE + if (addr == NR52_REG) { + bool new_power = (val & 0x80) != 0; + if (apu.power && !new_power) { + // POWER-OFF CLEARS ALL REGISTERS (EXCEPT WAVE RAM) + u8 saved_wave[16]; + memcpy(saved_wave, apu.wave_ram, sizeof(saved_wave)); + memset(&apu, 0, sizeof(apu)); + memcpy(apu.wave_ram, saved_wave, sizeof(saved_wave)); + // RESET HPF SO IT DOESN'T CARRY A STEP TRANSIENT + hpf_prev_in_l = hpf_prev_out_l = 0.0f; + hpf_prev_in_r = hpf_prev_out_r = 0.0f; + } + apu.power = new_power; + return; + } + + // ALL OTHER WRITES IGNORED WHILE POWERED OFF + if (!apu.power) return; + + switch (addr) { + // CH1 + case NR10_REG: apu.nr10 = val; break; + case NR11_REG: + apu.nr11 = val; + apu.ch1_length = 64 - (val & 0x3F); + break; + case NR12_REG: + apu.nr12 = val; + apu.ch1_env_period = val & 0x07; + if ((val & 0xF8) == 0) apu.ch1_enabled = false; // DAC OFF + break; + case NR13_REG: apu.nr13 = val; break; + case NR14_REG: + apu.nr14 = val; + apu.ch1_length_enable = (val & 0x40) != 0; + if (val & 0x80) trigger_ch1(); + break; + + // CH2 + case NR21_REG: + apu.nr21 = val; + apu.ch2_length = 64 - (val & 0x3F); + break; + case NR22_REG: + apu.nr22 = val; + apu.ch2_env_period = val & 0x07; + if ((val & 0xF8) == 0) apu.ch2_enabled = false; + break; + case NR23_REG: apu.nr23 = val; break; + case NR24_REG: + apu.nr24 = val; + apu.ch2_length_enable = (val & 0x40) != 0; + if (val & 0x80) trigger_ch2(); + break; + + // CH3 + case NR30_REG: + apu.nr30 = val; + if (!(val & 0x80)) apu.ch3_enabled = false; + break; + case NR31_REG: + apu.nr31 = val; + apu.ch3_length = 256 - val; + break; + case NR32_REG: apu.nr32 = val; break; + case NR33_REG: apu.nr33 = val; break; + case NR34_REG: + apu.nr34 = val; + apu.ch3_length_enable = (val & 0x40) != 0; + if (val & 0x80) trigger_ch3(); + break; + + // CH4 + case NR41_REG: + apu.nr41 = val; + apu.ch4_length = 64 - (val & 0x3F); + break; + case NR42_REG: + apu.nr42 = val; + apu.ch4_env_period = val & 0x07; + if ((val & 0xF8) == 0) apu.ch4_enabled = false; + break; + case NR43_REG: apu.nr43 = val; break; + case NR44_REG: + apu.nr44 = val; + apu.ch4_length_enable = (val & 0x40) != 0; + if (val & 0x80) trigger_ch4(); + break; + + // CONTROL + case NR50_REG: apu.nr50 = val; break; + case NR51_REG: apu.nr51 = val; break; + } +} diff --git a/src/bus.c b/src/bus.c index 08f0b36..506b3fc 100644 --- a/src/bus.c +++ b/src/bus.c @@ -1,5 +1,7 @@ #include "logger.h" #include "bus.h" +#include "joypad.h" +#include "apu.h" // MEMORY MAP /* @@ -9,20 +11,13 @@ CHECK BUS.H HEADER FILE, LET KEEP ALL INDEX/LOCATIONS THERE. static struct { u8 wram[0x2000]; // WORKING RAM (8KB) u8 hram[0x80]; // HIGH RAM (127B) - u8 io[0x80]; // IO REGISTERS (127B) + u8 io[0x80]; // IO REGISTERS (127B) u8 bootrom[0x100]; // BOOT ROM (256B) - - // IO PORTS + + // SERIAL PORTS - JOYPAD AND AUDIO OWN THEIR OWN STATE struct { - u8 joypad; u8 serial_data; u8 serial_control; - u8 nr10, nr11, nr12, nr13, nr14; // CHANNEL 1 - u8 nr21, nr22, nr23, nr24; // CHANNEL 2 - u8 nr30, nr31, nr32, nr33, nr34; // CHANNEL 3 - u8 nr41, nr42, nr43, nr44; // CHANNEL 4 - u8 nr50, nr51, nr52; // CONTROL - u8 wave_ram[16]; // WAVE PATTERN RAM } ports; } bus = {0}; @@ -38,7 +33,7 @@ static u8 read_io(u16 addr) { // HANDLE ALL IO PORT READS switch(addr) { // JOYPAD - case P1_REG: return bus.ports.joypad; + case P1_REG: return joypad_read(); case SB_REG: return bus.ports.serial_data; case SC_REG: return bus.ports.serial_control; @@ -51,40 +46,10 @@ static u8 read_io(u16 addr) { // INTERRUPTS case IF_REG: return get_interrupt_flags(); - // SOUND CH1 - case NR10_REG: return bus.ports.nr10; - case NR11_REG: return bus.ports.nr11; - case NR12_REG: return bus.ports.nr12; - case NR13_REG: return bus.ports.nr13; - case NR14_REG: return bus.ports.nr14; - - // SOUND CH2 - case NR21_REG: return bus.ports.nr21; - case NR22_REG: return bus.ports.nr22; - case NR23_REG: return bus.ports.nr23; - case NR24_REG: return bus.ports.nr24; - - // SOUND CH3 - case NR30_REG: return bus.ports.nr30; - case NR31_REG: return bus.ports.nr31; - case NR32_REG: return bus.ports.nr32; - case NR33_REG: return bus.ports.nr33; - case NR34_REG: return bus.ports.nr34; - - // SOUND CH4 - case NR41_REG: return bus.ports.nr41; - case NR42_REG: return bus.ports.nr42; - case NR43_REG: return bus.ports.nr43; - case NR44_REG: return bus.ports.nr44; - - // SOUND CTRL - case NR50_REG: return bus.ports.nr50; - case NR51_REG: return bus.ports.nr51; - case NR52_REG: return bus.ports.nr52; - - // WAVE PATTERN RAM + // SOUND REGISTERS + WAVE RAM + case NR10_REG ... NR52_REG: case WAVE_RAM_START ... WAVE_RAM_START + 0xF: - return bus.ports.wave_ram[addr - WAVE_RAM_START]; + return apu_read(addr); // LCD/PPU case LCDC_REG: @@ -118,7 +83,7 @@ static void write_io(u16 addr, u8 val) { break; // JOYPAD - case P1_REG: bus.ports.joypad = val; break; + case P1_REG: joypad_write(val); break; case SB_REG: bus.ports.serial_data = val; break; case SC_REG: bus.ports.serial_control = val; break; @@ -131,40 +96,10 @@ static void write_io(u16 addr, u8 val) { // INTERRUPTS case IF_REG: set_interrupt_flags(val); break; - // SOUND CH1 - case NR10_REG: bus.ports.nr10 = val; break; - case NR11_REG: bus.ports.nr11 = val; break; - case NR12_REG: bus.ports.nr12 = val; break; - case NR13_REG: bus.ports.nr13 = val; break; - case NR14_REG: bus.ports.nr14 = val; break; - - // SOUND CH2 - case NR21_REG: bus.ports.nr21 = val; break; - case NR22_REG: bus.ports.nr22 = val; break; - case NR23_REG: bus.ports.nr23 = val; break; - case NR24_REG: bus.ports.nr24 = val; break; - - // SOUND CH3 - case NR30_REG: bus.ports.nr30 = val; break; - case NR31_REG: bus.ports.nr31 = val; break; - case NR32_REG: bus.ports.nr32 = val; break; - case NR33_REG: bus.ports.nr33 = val; break; - case NR34_REG: bus.ports.nr34 = val; break; - - // SOUND CH4 - case NR41_REG: bus.ports.nr41 = val; break; - case NR42_REG: bus.ports.nr42 = val; break; - case NR43_REG: bus.ports.nr43 = val; break; - case NR44_REG: bus.ports.nr44 = val; break; - - // SOUND CTRL - case NR50_REG: bus.ports.nr50 = val; break; - case NR51_REG: bus.ports.nr51 = val; break; - case NR52_REG: bus.ports.nr52 = val; break; - - // WAVE PATTERN RAM + // SOUND REGISTERS + WAVE RAM + case NR10_REG ... NR52_REG: case WAVE_RAM_START ... WAVE_RAM_START + 0xF: - bus.ports.wave_ram[addr - WAVE_RAM_START] = val; + apu_write(addr, val); break; // LCD/PPU diff --git a/src/cart.c b/src/cart.c index 61ab30b..eedb208 100644 --- a/src/cart.c +++ b/src/cart.c @@ -637,6 +637,23 @@ bool save_battery(void) { return true; } +void cart_cleanup(void) { + if (c.has_battery) { + save_battery(); + } + if (c.rom_data) { + free(c.rom_data); + c.rom_data = NULL; + } + if (c.ram_data) { + free(c.ram_data); + c.ram_data = NULL; + } + c.header = NULL; + c.rom_size = 0; + c.ram_size = 0; +} + bool load_battery(void) { // NO BATTERY = NO LOAD if (!c.has_battery || !c.ram_data || c.ram_size == 0) { diff --git a/src/cpu.c b/src/cpu.c index 0e51a2d..044f9fd 100644 --- a/src/cpu.c +++ b/src/cpu.c @@ -68,7 +68,9 @@ static void set_reg_or_hl(u8 reg_idx, u8 value) { // DEBUGGING ----------------------------------------------------------------------- +__attribute__((unused)) static void debug_opcode_state(bool is_before) { + (void)is_before; LOG_DEBUG(LOG_CPU, "\n--- %s [0x%02X] @ PC:0x%04X ---", is_before ? "BEFORE" : "AFTER", CPU.opcode, CPU.reg.pc); @@ -636,7 +638,7 @@ static void dec_r(void) { static void add_hl_rr(void) { u8 reg_pair = (CPU.opcode >> 4) & 0x03; u16 hl = MAKE_WORD(CPU.reg.h, CPU.reg.l); - u16 value; + u16 value = 0; switch (reg_pair) { case 0x00: value = MAKE_WORD(CPU.reg.b, CPU.reg.c); break; // BC @@ -1024,7 +1026,8 @@ static void stop(void) { // DI - DISABLE INTERRUPTS static void di(void) { - CPU.ime = 0; // INTERRUPT MASTER ENABLE FLAG + CPU.ime = 0; // INTERRUPT MASTER ENABLE FLAG + CPU.ime_scheduled = 0; // CANCEL ANY PENDING EI (DI/EI RACE) CPU.cycles = 4; } @@ -1048,12 +1051,6 @@ static void cpl(void) { // JP nn - JUMP TO ABSOLUTE ADDRESS NN static void jp_nn(void) { - // GET CURRENT STATE - u16 old_pc = CPU.reg.pc; - u8 old_h = CPU.reg.h; - u8 old_l = CPU.reg.l; - - // PERFORM JUMP u16 addr = read_word(); CPU.reg.pc = addr; CPU.cycles = 16; @@ -1062,7 +1059,7 @@ static void jp_nn(void) { // JP cc,nn - CONDITIONAL JUMP TO ABSOLUTE ADDRESS NN static void jp_cc_nn(void) { u8 condition = (CPU.opcode >> 3) & 0x03; - bool jump; + bool jump = false; switch (condition) { case 0: jump = !GET_FLAG_Z; break; // NZ @@ -1097,7 +1094,7 @@ static void jr_n(void) { // JR cc,n - CONDITIONAL RELATIVE JUMP BY SIGNED IMMEDIATE N static void jr_cc_n(void) { u8 condition = (CPU.opcode >> 3) & 0x03; - bool jump; + bool jump = false; switch (condition) { case 0: jump = !GET_FLAG_Z; break; // NZ @@ -1137,7 +1134,7 @@ static void call_nn(void) { static void call_cc_nn(void) { u8 condition = (CPU.opcode >> 3) & 0x03; // EXTRACT CONDITION BITS u16 addr = read_word(); // FETCH ABSOLUTE ADDRESS NN - bool jump; + bool jump = false; // CHECK CONDITIONS switch (condition) { @@ -1207,7 +1204,7 @@ static void ret(void) { // RET cc - CONDITIONAL RETURN FROM SUBROUTINE static void ret_cc(void) { u8 condition = (CPU.opcode >> 3) & 0x03; - bool do_ret; + bool do_ret = false; switch (condition) { case 0: do_ret = !GET_FLAG_Z; break; // NZ @@ -1244,19 +1241,14 @@ static void reti(void) { // DAA void daa() { uint8_t adjust = 0; - bool carry_out = false; + bool carry_out = GET_FLAG_C; // PRESERVE C BY DEFAULT (SUBTRACT MODE) + if (GET_FLAG_N) { - if (GET_FLAG_H) { - adjust |= 0x06; - } - if (GET_FLAG_C) { - adjust |= 0x60; - } + if (GET_FLAG_H) adjust |= 0x06; + if (GET_FLAG_C) adjust |= 0x60; CPU.reg.a -= adjust; } else { - if (GET_FLAG_H || (CPU.reg.a & 0x0F) > 0x09) { - adjust |= 0x06; - } + if (GET_FLAG_H || (CPU.reg.a & 0x0F) > 0x09) adjust |= 0x06; if (GET_FLAG_C || CPU.reg.a > 0x99) { adjust |= 0x60; carry_out = true; @@ -1267,6 +1259,7 @@ void daa() { SET_FLAG_H(false); SET_FLAG_Z(CPU.reg.a == 0); SET_FLAG_C(carry_out); + CPU.cycles = 4; } // TABLE INITIALIZATION ------------------------------------------------------------ @@ -1476,29 +1469,28 @@ void cpu_init(void) { void cpu_step(void) { if (CPU.halted) { - return; + // ANY PENDING INTERRUPT WAKES HALT REGARDLESS OF IME. + // IME ONLY GATES WHETHER THE HANDLER IS SERVICED. + if (get_interrupt_flags() & get_interrupt_enable()) { + CPU.halted = false; + } else { + CPU.cycles = 4; // KEEP TIME ADVANCING WHILE WAITING + return; + } } - #ifdef DEBUG - // FOR PARSED TEST LOGS +#if LOG_VERBOSE_CPU + // FOR PARSED TEST LOGS - VERY EXPENSIVE, ONLY ENABLE WHEN DIFFING TRACES u8 mem[4]; for(int i = 0; i < 4; i++) { mem[i] = read_from_bus(CPU.reg.pc + i); } LOG_TEST("A:%02X F:%02X B:%02X C:%02X D:%02X E:%02X H:%02X L:%02X SP:%04X PC:%04X PCMEM:%02X,%02X,%02X,%02X", - CPU.reg.a, - CPU.reg.f, - CPU.reg.b, - CPU.reg.c, - CPU.reg.d, - CPU.reg.e, - CPU.reg.h, - CPU.reg.l, - CPU.reg.sp, - CPU.reg.pc, - mem[0], mem[1], mem[2], mem[3] - ); - #endif + CPU.reg.a, CPU.reg.f, CPU.reg.b, CPU.reg.c, + CPU.reg.d, CPU.reg.e, CPU.reg.h, CPU.reg.l, + CPU.reg.sp, CPU.reg.pc, + mem[0], mem[1], mem[2], mem[3]); +#endif // SET FLAG FOR IME SCHEDULER bool was_scheduled = CPU.ime_scheduled; diff --git a/src/dma.c b/src/dma.c index 0861c6a..e10d1b1 100644 --- a/src/dma.c +++ b/src/dma.c @@ -1,6 +1,7 @@ #include "logger.h" #include "dma.h" #include "bus.h" +#include "graphics.h" static struct dma DMA = {0}; @@ -21,10 +22,10 @@ void dma_tick() { return; } - // TRANSFER ONE BYTE FROM SOURCE TO OAM + // TRANSFER ONE BYTE FROM SOURCE TO OAM DIRECTLY (BUS READ + OAM WRITE) u16 source = (DMA.val << 8) + DMA.byte; - write_to_bus(OAM_START + DMA.byte, read_from_bus(source)); - + oam_write(OAM_START + DMA.byte, read_from_bus(source)); + DMA.byte++; DMA.isActive = DMA.byte < 0xA0; // 160 BYTES TOTAL } diff --git a/src/gameboy.c b/src/gameboy.c index e66492b..362b2a3 100644 --- a/src/gameboy.c +++ b/src/gameboy.c @@ -1,5 +1,8 @@ #include "logger.h" #include "gameboy.h" +#include "joypad.h" +#include "apu.h" +#include "cart.h" #include #include @@ -16,19 +19,22 @@ void gameboy_init(bool bootrom_enabled) { GB.bootrom_enabled = bootrom_enabled; // NOTE: DONT TRY TO INIT RAM HERE, IT SHOULD ALREADY BE DONE - interrupt_init(); // INTERRUPTS FIRST + interrupt_init(); // INTERRUPTS FIRST timer_init(); // THEN OTHER SUBSYSTEMS cpu_init(); - graphics_init(); + joypad_init(); + graphics_init(); // ALSO INITS SDL (VIDEO + AUDIO) + apu_init(); // NEEDS SDL_INIT_AUDIO PERF_FREQ = SDL_GetPerformanceFrequency(); TICKS_PER_FRAME = PERF_FREQ / 60; // TARGET 60 FPS, THIS IS MUCH BETTER } void gameboy_destroy() { - // UNINITIALIZE STUFF HERE LOG_DEBUG(LOG_MAIN, "CLEANING UP GAMEBOY MEMORY, GRAPHICS, ETC.\n"); + apu_cleanup(); graphics_cleanup(); + cart_cleanup(); // PERSISTS BATTERY SAVE AND FREES ROM/RAM } void run_gb() { @@ -41,18 +47,18 @@ void run_gb() { // SYNC TO 4194304 CYCLES PER SECOND (59.7275 HZ) while (GB.cycles_this_frame < CYCLES_PER_FRAME) { if (!GB.paused) { - cpu_step(); - handle_interrupts(); - u8 cycles = get_cpu_cycles(); + cycles += handle_interrupts(); // ADDS 20 T-CYCLES IF SERVICED + GB.cycles_this_frame += cycles; - + // RUN PPU, TIMER, DMA FOR EACH M-CYCLE (4 T-CYCLES) for (int t = 0; t < cycles; t += 4) { timer_tick(); dma_tick(); graphics_tick(); + apu_tick(); } } } diff --git a/src/graphics.c b/src/graphics.c index 906de08..adb7cee 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -2,6 +2,7 @@ #include "graphics.h" #include "interrupt.h" #include "bus.h" +#include "joypad.h" static graphics_system graphics; @@ -48,6 +49,7 @@ void graphics_cleanup() { SDL_Quit(); } +#if DEBUG_WINDOW static u8 get_tile_pixel(u8 tile_idx, u8 x, u8 y) { u16 tile_addr = (tile_idx * 16) + (y * 2); // CALCULATE ADDRESS OF TILE DATA @@ -61,6 +63,7 @@ static u8 get_tile_pixel(u8 tile_idx, u8 x, u8 y) { // COMBINE BITS TO GET COLOR INDEX return ((byte2 >> bit_pos) & 1) << 1 | ((byte1 >> bit_pos) & 1); } +#endif // RENDER BACKGROUND AND WINDOW static void render_background_line() { @@ -104,16 +107,23 @@ static void render_background_line() { const u8 bit_pos = 7 - tile_x_offset; const u8 color_idx = ((high_byte >> bit_pos) & 1) << 1 | ((low_byte >> bit_pos) & 1); - // WRITE PIXEL TO FRAME BUFFER + // WRITE PIXEL TO FRAME BUFFER + REMEMBER BG INDEX FOR SPRITE PRIORITY graphics.frame_buffer[(graphics.line * SCREEN_WIDTH) + screen_x] = graphics.bg_colors[color_idx]; + graphics.bg_index_line[screen_x] = color_idx; } + } else { + // BG DISABLED -> ALL PIXELS COUNT AS COLOR 0 FOR PRIORITY + for (int x = 0; x < SCREEN_WIDTH; x++) graphics.bg_index_line[x] = 0; } // RENDER WINDOW IF ENABLED AND WITHIN Y-BOUNDS - if ((graphics.lcdc & LCDC_WINDOW_ENABLE) && graphics.wy <= graphics.line) { - const u8 window_y = graphics.line - graphics.wy; // CURRENT Y IN WIN MAP - const u8 tile_row = window_y / 8; // CURRENT ROW IN TILE MAP - const u8 tile_y_offset = window_y % 8; // CURRENT Y OFFSET IN TILE + if ((graphics.lcdc & LCDC_WINDOW_ENABLE) && graphics.wy <= graphics.line && graphics.wx < 167) { + // WINDOW HAS ITS OWN INTERNAL LINE COUNTER THAT ONLY ADVANCES + // ON SCANLINES THE WINDOW ACTUALLY RENDERED. + const u8 window_y = graphics.window_line; + const u8 tile_row = window_y / 8; + const u8 tile_y_offset = window_y % 8; + graphics.window_line++; // PREPARE MAP ADDRESS const u16 window_map_base = BG_MAP_ADDRESSES[(graphics.lcdc & LCDC_WINDOW_MAP) >> 6]; @@ -145,128 +155,88 @@ static void render_background_line() { const u8 bit_pos = 7 - tile_x_offset; const u8 color_idx = ((high_byte >> bit_pos) & 1) << 1 | ((low_byte >> bit_pos) & 1); - // WRITE PIXEL TO FRAME BUFFER + // WRITE PIXEL TO FRAME BUFFER + UPDATE BG INDEX graphics.frame_buffer[(graphics.line * SCREEN_WIDTH) + screen_x] = graphics.bg_colors[color_idx]; + graphics.bg_index_line[screen_x] = color_idx; } } } -// SORT SPRITES BY X-COORDINATE FOR DRAWING PRIORITY -static void sort_sprites_by_x(const sprite* source, sprite* dest, int sprite_count) { - // COPY SOURCE TO DESTINATION ARRAY - memcpy(dest, source, sizeof(sprite) * sprite_count); - - // BUBBLE SORT BY X-COORDINATE (ASCENDING) - for (int i = 0; i < sprite_count - 1; i++) { - for (int j = 0; j < sprite_count - i - 1; j++) { - if (dest[j].x > dest[j + 1].x) { - sprite temp = dest[j]; - dest[j] = dest[j + 1]; - dest[j + 1] = temp; - } - } - } -} - -// RENDER SPRITES FOR THE CURRENT SCANLINE +// RENDER SPRITES FOR THE CURRENT SCANLINE. +// DMG RULES: +// 1) AT MOST 10 SPRITES PER SCANLINE - PICKED IN OAM ORDER, NOT BY X. +// 2) LOWER X DRAWS ON TOP OF HIGHER X. WITHIN EQUAL X, LOWER OAM INDEX WINS. +// 3) IF SPRITE FLAG BIT 7 (BG PRIORITY) IS SET, THE SPRITE IS HIDDEN +// WHEREVER THE BG COLOR INDEX IS NON-ZERO. static void render_sprites_line() { - // EXIT EARLY IF SPRITES ARE DISABLED if (!(graphics.lcdc & LCDC_OBJ_ENABLE)) return; - // DETERMINE SPRITE HEIGHT (8 OR 16 PIXELS) const u8 sprite_height = (graphics.lcdc & LCDC_OBJ_SIZE) ? 16 : 8; - - // COUNTER FOR VISIBLE SPRITES ON THE CURRENT LINE + + // STEP 1: PICK FIRST 10 VISIBLE SPRITES IN OAM ORDER + sprite line_sprites[SPRITES_PER_LINE]; int sprite_count = 0; - - // TEMPORARY ARRAY FOR SPRITES - sprite temp_sprites[MAX_SPRITES]; - // ITERATE OVER ALL SPRITES TO FIND VISIBLE ONES - for (int i = 0; i < MAX_SPRITES; i++) { - // CALCULATE SPRITE Y-POSITION ON SCREEN + for (int i = 0; i < MAX_SPRITES && sprite_count < SPRITES_PER_LINE; i++) { const int sprite_y = graphics.oam[i].y - 16; - - // CHECK IF SPRITE IS VISIBLE ON THE CURRENT LINE + X BOUNDS - if (graphics.line >= sprite_y && - graphics.line < sprite_y + sprite_height && - graphics.oam[i].x > 0 && - graphics.oam[i].x < SCREEN_WIDTH + 8) { - - // STORE SPRITE IF VISIBLE - temp_sprites[sprite_count++] = graphics.oam[i]; + if (graphics.line >= sprite_y && graphics.line < sprite_y + sprite_height) { + line_sprites[sprite_count++] = graphics.oam[i]; } } - // SORT SPRITES BY X-COORDINATE FOR CORRECT DRAWING PRIORITY - sort_sprites_by_x(temp_sprites, temp_sprites, sprite_count); - - // LIMIT TO 10 SPRITES PER LINE - const int max_sprites_per_line = (sprite_count > SPRITES_PER_LINE) ? SPRITES_PER_LINE : sprite_count; - - // ITERATE OVER VISIBLE SPRITES AND DRAW THEM - for (int i = 0; i < max_sprites_per_line; i++) { - const sprite* s = &temp_sprites[i]; + // STEP 2: STABLE-SORT ASCENDING BY X SO WE CAN ITERATE IN REVERSE BELOW. + // STABLE SORT PRESERVES OAM ORDER WHEN X IS EQUAL, AND DRAWING IN REVERSE + // MEANS LOWER OAM INDEX (DRAWN LAST AMONG EQUAL-X) ENDS UP ON TOP. + for (int i = 1; i < sprite_count; i++) { + sprite key = line_sprites[i]; + int j = i - 1; + while (j >= 0 && line_sprites[j].x > key.x) { + line_sprites[j + 1] = line_sprites[j]; + j--; + } + line_sprites[j + 1] = key; + } - // CALCULATE SPRITE X-POSITION ON SCREEN - const int sprite_x = s->x - 8; + // STEP 3: DRAW HIGHEST-X FIRST, LOWEST-X LAST (LOWEST X ENDS UP ON TOP) + for (int i = sprite_count - 1; i >= 0; i--) { + const sprite* s = &line_sprites[i]; + const int sprite_x = (int)s->x - 8; - // SKIP OFF-SCREEN OR HIDDEN SPRITES - if (sprite_x < -7 || sprite_x >= SCREEN_WIDTH) continue; + if (s->x == 0 || sprite_x >= SCREEN_WIDTH) continue; - // DETERMINE Y-FLIP AND SELECT CORRECT PALETTE const bool y_flip = (s->flags & 0x40) != 0; const u8 palette = (s->flags & 0x10) ? 1 : 0; + const bool bg_priority = (s->flags & 0x80) != 0; - // CALCULATE CURRENT LINE WITHIN THE SPRITE - u8 sprite_line = graphics.line - (s->y - 16); - if (y_flip) { - sprite_line = (sprite_height - 1) - sprite_line; - } + u8 sprite_line = (u8)((int)graphics.line - ((int)s->y - 16)); + if (y_flip) sprite_line = (sprite_height - 1) - sprite_line; - // ADJUST TILE INDEX FOR 8X16 MODE u8 tile_index = s->tile; if (sprite_height == 16) { - tile_index &= 0xFE; // USE EVEN TILE INDEX + tile_index &= 0xFE; // EVEN TILE BASE if (sprite_line >= 8) { - tile_index++; // USE NEXT TILE FOR LOWER HALF + tile_index++; // BOTTOM HALF -> NEXT TILE sprite_line -= 8; } } - // CALCULATE TILE DATA ADDRESS const u16 tile_data_address = VRAM_START + (tile_index * 16) + (sprite_line * 2); - - // READ TILE DATA BYTES - const u8 low_byte = read_from_bus(tile_data_address); + const u8 low_byte = read_from_bus(tile_data_address); const u8 high_byte = read_from_bus(tile_data_address + 1); - // ITERATE OVER PIXELS IN THE CURRENT SPRITE LINE for (int px = 0; px < 8; px++) { - // CALCULATE X-COORDINATE ON SCREEN const int screen_x = sprite_x + px; - - // SKIP PIXELS OUTSIDE OF SCREEN BOUNDS if (screen_x < 0 || screen_x >= SCREEN_WIDTH) continue; - // DETERMINE X-FLIP + COLOR INDEX FROM TILE DATA const u8 bit_pos = (s->flags & 0x20) ? px : (7 - px); const u8 color_idx = (((high_byte >> bit_pos) & 1) << 1) | ((low_byte >> bit_pos) & 1); + if (color_idx == 0) continue; // SPRITE-TRANSPARENT - // SKIP TRANSPARENT PIXELS (COLOR INDEX 0) - if (color_idx == 0) continue; + // BG PRIORITY: SPRITE HIDDEN WHERE BG INDEX IS 1..3 + if (bg_priority && graphics.bg_index_line[screen_x] != 0) continue; - // CALCULATE FRAME BUFFER INDEX const int fb_index = (graphics.line * SCREEN_WIDTH) + screen_x; - - // PRIORITY CHECK HANDLING - const bool bg_priority = (s->flags & 0x80) != 0; - const u32 current_color = graphics.frame_buffer[fb_index]; - - // DRAW PIXEL IF IT PASSES PRIORITY CHECK - if (!bg_priority || current_color == graphics.bg_colors[0]) { - graphics.frame_buffer[fb_index] = graphics.sprite_colors[palette][color_idx]; - } + graphics.frame_buffer[fb_index] = graphics.sprite_colors[palette][color_idx]; } } } @@ -276,7 +246,10 @@ void render_line() { // EXIT EARLY IF LCD IS DISABLED OR LINE IS INVALID if (!(graphics.lcdc & LCDC_ENABLE)) { const int fb_start = graphics.line * SCREEN_WIDTH; - memset(&graphics.frame_buffer[fb_start], 0xFF, SCREEN_WIDTH * sizeof(u32)); + const u32 white = graphics.bg_colors[0]; + for (int x = 0; x < SCREEN_WIDTH; x++) { + graphics.frame_buffer[fb_start + x] = white; + } return; } @@ -293,9 +266,11 @@ void render_line() { } void draw_frame() { - // FRAME COUNTER +#if DEBUG_WINDOW + // FRAME COUNTER FOR DEBUG WINDOW PACING static int frame_count = 0; frame_count++; +#endif // CLEAR RENDERER WITH WHITE BACKGROUND FOR EMPTY AREAS SDL_SetRenderDrawColor(graphics.renderer, 255, 255, 255, 255); @@ -321,15 +296,17 @@ void draw_frame() { // PRESENT RENDERER TO THE WINDOW SDL_RenderPresent(graphics.renderer); +#if DEBUG_WINDOW // PERIODICALLY UPDATE DEBUG WINDOW if (frame_count % 30 == 0) { update_debug_window(); } +#endif } void graphics_init() { // INIT SDL WITH ERROR CHECKING - if (SDL_Init(SDL_INIT_VIDEO) < 0) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { LOG_ERROR(LOG_GRAPHICS, "SDL INIT FAILED: %s\n", SDL_GetError()); exit(1); } @@ -417,13 +394,14 @@ void graphics_init() { exit(1); } - // DEBUG WINDOW SETUP +#if DEBUG_WINDOW + // DEBUG WINDOW SETUP - TILE ATLAS VIEWER int main_x, main_y, main_w, main_h; SDL_GetWindowPosition(graphics.window, &main_x, &main_y); SDL_GetWindowSize(graphics.window, &main_w, &main_h); - + graphics.debug_window = SDL_CreateWindow( - "DEBUGDEEZ NUTZ", + "GBEmu - Tile Atlas", main_x + main_w + 10, main_y, 256 * 2, @@ -442,7 +420,7 @@ void graphics_init() { graphics.debug_renderer = SDL_CreateRenderer(graphics.debug_window, -1, SDL_RENDERER_ACCELERATED); - + if (!graphics.debug_renderer) { LOG_ERROR(LOG_GRAPHICS, "DEBUG RENDERER CREATE FAILED: %s\n", SDL_GetError()); SDL_DestroyWindow(graphics.debug_window); @@ -470,8 +448,9 @@ void graphics_init() { SDL_Quit(); exit(1); } +#endif - LOG_ERROR(LOG_GRAPHICS, "GRAPHICS INIT COMPLETE - VIDEO MODE: %s\n", SDL_GetCurrentVideoDriver()); + LOG_INFO(LOG_GRAPHICS, "GRAPHICS INIT COMPLETE - VIDEO MODE: %s\n", SDL_GetCurrentVideoDriver()); dump_frame_buffer_sample(); } @@ -480,12 +459,16 @@ void graphics_tick() { // FILL FRAME BUFFER WITH WHITE IF WE HAVEN'T ALREADY static bool cleared = false; if (!cleared) { - memset(graphics.frame_buffer, 0xFF, sizeof(graphics.frame_buffer)); + const u32 white = graphics.bg_colors[0]; + for (size_t i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) { + graphics.frame_buffer[i] = white; + } cleared = true; } // RESET PPU STATE graphics.line = 0; graphics.ly = 0; + graphics.window_line = 0; graphics.mode = MODE_HBLANK; graphics.stat &= ~0x03; graphics.mode_clock = 0; @@ -562,23 +545,61 @@ void graphics_tick() { } } +// KEY -> GB BUTTON MAPPING IS CONFIGURED IN include/config.h +// EACH GB BUTTON HAS A PRIMARY AND ALTERNATE KEY. UNASSIGNED SLOTS USE +// SDLK_UNKNOWN AND ARE IGNORED HERE. +static gb_button key_to_button(SDL_Keycode k) { + if (k == SDLK_UNKNOWN) return (gb_button)0; + + if (k == KEY_GB_RIGHT_PRI || k == KEY_GB_RIGHT_ALT) return BTN_RIGHT; + if (k == KEY_GB_LEFT_PRI || k == KEY_GB_LEFT_ALT) return BTN_LEFT; + if (k == KEY_GB_UP_PRI || k == KEY_GB_UP_ALT) return BTN_UP; + if (k == KEY_GB_DOWN_PRI || k == KEY_GB_DOWN_ALT) return BTN_DOWN; + if (k == KEY_GB_A_PRI || k == KEY_GB_A_ALT) return BTN_A; + if (k == KEY_GB_B_PRI || k == KEY_GB_B_ALT) return BTN_B; + if (k == KEY_GB_SELECT_PRI || k == KEY_GB_SELECT_ALT) return BTN_SELECT; + if (k == KEY_GB_START_PRI || k == KEY_GB_START_ALT) return BTN_START; + return (gb_button)0; +} + void handle_events() { SDL_Event event; - while (SDL_PollEvent(&event)) + while (SDL_PollEvent(&event)) { - switch (event.type) + switch (event.type) { case SDL_QUIT: get_gb()->die = true; break; + case SDL_KEYDOWN: + if (event.key.repeat) break; // IGNORE OS KEY-REPEAT + if (event.key.keysym.sym == KEY_GB_QUIT) { + get_gb()->die = true; + break; + } + { + gb_button b = key_to_button(event.key.keysym.sym); + if (b) joypad_press(b); + } + break; + + case SDL_KEYUP: + { + gb_button b = key_to_button(event.key.keysym.sym); + if (b) joypad_release(b); + } + break; + case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_CLOSE) { - // DISTINGUISH DEBUG VS MAIN WINDOW +#if DEBUG_WINDOW + // CLOSING THE DEBUG WINDOW JUST HIDES IT if (event.window.windowID == SDL_GetWindowID(graphics.debug_window)) { SDL_HideWindow(graphics.debug_window); - } - else if (event.window.windowID == SDL_GetWindowID(graphics.window)) { + } else +#endif + if (event.window.windowID == SDL_GetWindowID(graphics.window)) { get_gb()->die = true; } } @@ -633,10 +654,14 @@ void lcd_write(u16 addr, u8 val) { if (!(val & LCDC_ENABLE)) { graphics.line = 0; graphics.ly = 0; + graphics.window_line = 0; graphics.mode = MODE_HBLANK; graphics.mode_clock = 0; graphics.stat &= ~0x03; - memset(graphics.frame_buffer, 0xFF, SCREEN_WIDTH * SCREEN_HEIGHT * sizeof(u32)); + const u32 white = graphics.bg_colors[0]; + for (size_t i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) { + graphics.frame_buffer[i] = white; + } } break; } @@ -701,6 +726,10 @@ void lcd_write(u16 addr, u8 val) { } void update_debug_window() { +#if !DEBUG_WINDOW + // COMPILED OUT WHEN DEBUG_WINDOW=0 IN config.h + return; +#else static u32 debug_buffer[256 * 256]; static int frame_count = 0; frame_count++; @@ -753,6 +782,7 @@ void update_debug_window() { if (frame_count % 60 == 0) { LOG_TRACE(LOG_GRAPHICS, "Updated debug window frame %d\n", frame_count); } +#endif } void dump_frame_buffer_sample() { diff --git a/src/interrupt.c b/src/interrupt.c index 91e17dd..124ed2e 100644 --- a/src/interrupt.c +++ b/src/interrupt.c @@ -18,58 +18,35 @@ void interrupt_req(interrupts i) { interrupt.flags |= i; } -void handle_interrupts() { - if (!get_ime()) { - return; - } - +u8 handle_interrupts() { u8 active = interrupt.flags & interrupt.enable; - if (!active) return; + if (!active) return 0; + + // ANY PENDING INTERRUPT WAKES HALT REGARDLESS OF IME + set_cpu_halted(false); + + // BUT THE HANDLER ONLY DISPATCHES WHEN IME IS SET + if (!get_ime()) return 0; registers* reg = get_registers(); + u16 vector = 0; + u8 which = 0; + + // PRIORITY ORDER: VBLANK > LCD > TIMER > SERIAL > JOYPAD + if (active & INT_VBLANK) { which = INT_VBLANK; vector = 0x40; } + else if (active & INT_LCD) { which = INT_LCD; vector = 0x48; } + else if (active & INT_TIMER) { which = INT_TIMER; vector = 0x50; } + else if (active & INT_SERIAL) { which = INT_SERIAL; vector = 0x58; } + else if (active & INT_JOYPAD) { which = INT_JOYPAD; vector = 0x60; } + else return 0; + + interrupt.flags &= ~which; + set_ime(false); + stack_push16(reg->pc); + reg->pc = vector; - // HANDLE ONE INTERRUPT AT A TIME IN PRIORITY ORDER - // I DONT THINK WE ARE DOING THIS RIGHT, TOO AGGRESSIVE - if (active & INT_VBLANK) { - interrupt.flags &= ~INT_VBLANK; - LOG_INFO(LOG_INTERRUPT, "!! -- VBLANK INTERRUPT -- !!\n"); - set_ime(false); - stack_push16(reg->pc); - reg->pc = 0x40; - set_cpu_halted(false); - } - else if (active & INT_LCD) { - interrupt.flags &= ~INT_LCD; - LOG_INFO(LOG_INTERRUPT, "!! -- LCD INTERRUPT -- !!\n"); - set_ime(false); - stack_push16(reg->pc); - reg->pc = 0x48; - set_cpu_halted(false); - } - else if (active & INT_TIMER) { - interrupt.flags &= ~INT_TIMER; - LOG_INFO(LOG_INTERRUPT, "!! -- TIMER INTERRUPT -- !!\n"); - set_ime(false); - stack_push16(reg->pc); - reg->pc = 0x50; - set_cpu_halted(false); - } - else if (active & INT_SERIAL) { - interrupt.flags &= ~INT_SERIAL; - LOG_INFO(LOG_INTERRUPT, "!! -- SERIAL INTERRUPT -- !!\n"); - set_ime(false); - stack_push16(reg->pc); - reg->pc = 0x58; - set_cpu_halted(false); - } - else if (active & INT_JOYPAD) { - interrupt.flags &= ~INT_JOYPAD; - LOG_INFO(LOG_INTERRUPT, "!! -- JOYPAD INTERRUPT -- !!\n"); - set_ime(false); - stack_push16(reg->pc); - reg->pc = 0x60; - set_cpu_halted(false); - } + // ISR DISPATCH IS 5 M-CYCLES = 20 T-CYCLES + return 20; } u8 get_interrupt_flags() { diff --git a/src/joypad.c b/src/joypad.c new file mode 100644 index 0000000..aa68595 --- /dev/null +++ b/src/joypad.c @@ -0,0 +1,58 @@ +#include "joypad.h" +#include "interrupt.h" + +// P1 (FF00) LAYOUT: +// BIT 7-6: UNUSED (READ AS 1) +// BIT 5: 0 = ACTION BUTTONS SELECTED +// BIT 4: 0 = DIRECTION BUTTONS SELECTED +// BIT 3-0: BUTTON STATES OF THE SELECTED COLUMN (0 = PRESSED) +static u8 buttons; // 1 BIT PER LOGICAL BUTTON, 1 = PRESSED +static u8 column_select; // ONLY BITS 5 AND 4 ARE MEANINGFUL + +void joypad_init(void) { + buttons = 0; + column_select = 0x30; // BOTH COLUMNS DESELECTED +} + +static u8 compute_lower_nibble(void) { + u8 lower = 0x0F; // NOTHING PRESSED + + // BIT 4 LOW = DIRECTIONS SELECTED + if (!(column_select & 0x10)) { + if (buttons & BTN_RIGHT) lower &= ~0x01; + if (buttons & BTN_LEFT) lower &= ~0x02; + if (buttons & BTN_UP) lower &= ~0x04; + if (buttons & BTN_DOWN) lower &= ~0x08; + } + // BIT 5 LOW = ACTIONS SELECTED + if (!(column_select & 0x20)) { + if (buttons & BTN_A) lower &= ~0x01; + if (buttons & BTN_B) lower &= ~0x02; + if (buttons & BTN_SELECT) lower &= ~0x04; + if (buttons & BTN_START) lower &= ~0x08; + } + return lower; +} + +void joypad_press(gb_button b) { + u8 before = compute_lower_nibble(); + buttons |= (u8)b; + u8 after = compute_lower_nibble(); + // ANY 1->0 TRANSITION IN A SELECTED COLUMN RAISES INT_JOYPAD + if ((before & ~after) != 0) { + interrupt_req(INT_JOYPAD); + } +} + +void joypad_release(gb_button b) { + buttons &= (u8)~b; +} + +u8 joypad_read(void) { + return 0xC0 | (column_select & 0x30) | compute_lower_nibble(); +} + +void joypad_write(u8 val) { + // ONLY BITS 5 AND 4 ARE WRITABLE + column_select = val & 0x30; +} diff --git a/src/logger.c b/src/logger.c index 3ec800c..2573eb9 100644 --- a/src/logger.c +++ b/src/logger.c @@ -19,11 +19,12 @@ typedef struct { // SINGLETON static LogContext log_contexts[LOG_COUNT]; +#if LOG_TO_FILE // EACH PART GETS ITS OWN LOG FILE ON ROTATION static const char* component_names[] = { "main", "cpu", - "bus", + "bus", "graphics", "cart", "dma", @@ -65,27 +66,33 @@ static void check_rotate_log(LogComponent component) { } } } +#endif // LOG_TO_FILE // INIT IN MAIN void logger_init(LogLevel global_level) { +#if LOG_TO_FILE #ifdef _WIN32 mkdir(LOG_DIR); #else mkdir(LOG_DIR, 0777); #endif +#endif - #ifdef DEBUG +#if LOG_TO_STDOUT printf("INITIALIZING ALL LOGGERS WITH LEVEL: %d\n", global_level); - #endif +#endif for (int i = 0; i < LOG_COUNT; i++) { + log_contexts[i].level = global_level; + log_contexts[i].enabled = true; + log_contexts[i].file = NULL; + +#if LOG_TO_FILE char filepath[512]; snprintf(filepath, sizeof(filepath), "%s/%s.log", LOG_DIR, component_names[i]); - log_contexts[i].file = fopen(filepath, "a"); strncpy(log_contexts[i].filename, filepath, sizeof(log_contexts[i].filename)-1); - log_contexts[i].level = global_level; - log_contexts[i].enabled = true; +#endif } } @@ -101,9 +108,15 @@ void logger_cleanup(void) { // DONT USE, USE MACROS, SEE HEADER FILE void log_message(LogComponent component, LogLevel level, const char* fmt, ...) { +#if !(LOG_TO_FILE || LOG_TO_STDOUT) + // ALL SINKS DISABLED - SHOULD NEVER BE CALLED (MACROS ARE NO-OPS), + // BUT GUARD ANYWAY. + (void)component; (void)level; (void)fmt; + return; +#else LogContext* ctx = &log_contexts[component]; - - if (!ctx->enabled || level > ctx->level || !ctx->file) { + + if (!ctx->enabled || level > ctx->level) { return; } @@ -113,9 +126,7 @@ void log_message(LogComponent component, LogLevel level, const char* fmt, ...) { vsnprintf(message, sizeof(message), fmt, args); va_end(args); - if (level != LOG_TEST) { - time_t t = time(NULL); struct tm* tm = localtime(&t); char timestamp[32]; @@ -128,18 +139,27 @@ void log_message(LogComponent component, LogLevel level, const char* fmt, ...) { case LOG_INFO: level_str = "INFO"; break; case LOG_DEBUG: level_str = "DEBUG"; break; case LOG_TRACE: level_str = "TRACE"; break; + case LOG_TEST: break; // HANDLED BELOW } - #ifdef DEBUG + #if LOG_TO_STDOUT printf("[%s][%s] %s\n", timestamp, level_str, message); - #endif - - fprintf(ctx->file, "[%s][%s] %s\n", timestamp, level_str, message); - fflush(ctx->file); - check_rotate_log(component); + #endif + #if LOG_TO_FILE + if (ctx->file) { + fprintf(ctx->file, "[%s][%s] %s\n", timestamp, level_str, message); + fflush(ctx->file); + check_rotate_log(component); + } + #endif } else { - fprintf(ctx->file, "%s\n", message); - fflush(ctx->file); + #if LOG_TO_FILE + if (ctx->file) { + fprintf(ctx->file, "%s\n", message); + fflush(ctx->file); + } + #endif } +#endif } diff --git a/src/timer.c b/src/timer.c index 5f82599..f24a4b1 100644 --- a/src/timer.c +++ b/src/timer.c @@ -34,29 +34,22 @@ void timer_init(void) { } void timer_tick(void) { - // INCREMENT SYSTEM DIVIDER (ALWAYS RUNS) - timer.div++; - - // CHECK IF TIMER IS ENABLED - if (!(timer.tac & TAC_ENABLE)) { - timer.prev_bit = get_timer_bit(); - return; - } + // CALLED ONCE PER M-CYCLE (4 T-CYCLES). THE INTERNAL DIV COUNTER + // ADVANCES EVERY T-CYCLE SO WE LOOP 4X TO CATCH ALL FALLING EDGES. + for (int t = 0; t < 4; t++) { + timer.div++; + + bool current_bit = get_timer_bit() && (timer.tac & TAC_ENABLE); - // GET CURRENT BIT FOR FREQUENCY - bool current_bit = get_timer_bit(); - - // FALLING EDGE DETECTION (1->0 TRANSITION) - if (timer.prev_bit && !current_bit) { - // CHECK FOR OVERFLOW - if (++timer.tima == 0) { - timer.tima = timer.tma; // LOAD MODULO VALUE - interrupt_req(INT_TIMER); // REQUEST INTERRUPT - LOG_WARN(LOG_TIMER, "TIMER OVERFLOW - LOADED TMA=0x%02X\n", timer.tma); + // FALLING EDGE DETECTION (1->0 TRANSITION) + if (timer.prev_bit && !current_bit) { + if (++timer.tima == 0) { + timer.tima = timer.tma; // LOAD MODULO VALUE + interrupt_req(INT_TIMER); // REQUEST INTERRUPT + } } + timer.prev_bit = current_bit; } - - timer.prev_bit = current_bit; } u8 timer_read(u16 addr) { @@ -78,8 +71,16 @@ u8 timer_read(u16 addr) { void timer_write(u16 addr, u8 val) { switch(addr) { case DIV_REG: // 0xFF04 - timer.div = 0; // ANY WRITE RESETS DIV - LOG_INFO(LOG_TIMER, "DIV RESET TO 0\n"); + // WRITING DIV CAN CAUSE A TIMA TICK IF THE SELECTED BIT WAS HIGH + // (FALLING EDGE FROM 1 TO 0 AS THE COUNTER RESETS). + if (timer.prev_bit) { + if (++timer.tima == 0) { + timer.tima = timer.tma; + interrupt_req(INT_TIMER); + } + } + timer.div = 0; + timer.prev_bit = false; break; case TIMA_REG: // 0xFF05