diff --git a/MiSTer.ini b/MiSTer.ini index fe5acb5..d67ddaf 100644 --- a/MiSTer.ini +++ b/MiSTer.ini @@ -341,6 +341,10 @@ vrr_vesa_framerate=0 ; disable autofire if for some reason it's not required and accidentally triggered disable_autofire=0 +; custom autofire rates (in hertz). up to five allowed. +; can also use literal bit patterns for on/off cycles, i.e. 0b00111. each bit is one frame. +;autofire_rates=10,0b00111,20,30 + ; Specify a default video processing preset that will be applied to cores. ; Path is relative to the presets/ directory and can optionally include the .ini extension ;preset_default=General Hardware/Console - 3rdGen diff --git a/autofire.cpp b/autofire.cpp new file mode 100644 index 0000000..d8042ca --- /dev/null +++ b/autofire.cpp @@ -0,0 +1,342 @@ +#include +#include +#include +#include +#include +#include +#include "input.h" +#include "autofire.h" +#include "cfg.h" + +/* + New autofire system. + We take the desired autofire rate in hertz and convert that to a bitmask + 0 == button released + 1 == button pressed + We advance through a single bit of this mask every frame on the assumption that + most games read their inputs every vsync (or is it vrefresh? once per frame.) + + We display autofire rates to the user as if the game is running at 60hz, but internally + the rate is scaled to match the game's actual display refresh rate. + + e.g. A PAL game (running at 50hz) with 30hz autofire will internally be toggling the button at 25hz + actual_refresh / 60 * autofire_rate_hz == real_autofire_rate_hz +*/ + +#define MAX_AF_CODES 16 +#define MAX_AF_RATES 6 +#define AF_NAME_LEN 32 + +// global autofire cycle data. +struct AutofireData { + char name[AF_NAME_LEN]; // display name + uint64_t cycle_mask; // bitmask representing the autofire cycle + int cycle_length; // length of the cycle in frames + int bit; // current bit in the cycle + int frame_count; // current frame in the cycle +}; + +// per-player autofire table; index 0 means disabled. +struct AutofireTable { + int count; // number of entries + int index[MAX_AF_CODES]; // index of matching autofire rate in autofiredata[] + uint32_t mask[MAX_AF_CODES]; // bitmask representing which buttons this code represents + uint32_t autofirecodes[MAX_AF_CODES]; // codes that have autofire set (or codes that had it set, then disabled) +}; + +static struct AutofireTable autofiretable[NUMPLAYERS]; // tracks autofire state per key per player +static struct AutofireData autofiredata[MAX_AF_RATES]; // global autofire rates/masks/etc. +static struct AutofireData autofiredata_default[MAX_AF_RATES]; // hardcoded fallback rates/masks/etc. +static int num_af_rates_default = 0; +static int num_af_rates = 0; + +static void set_autofire_name(struct AutofireData *data, const char *base_name) { + float hz = data->cycle_length > 0 ? (60.0f / data->cycle_length) : 0.0f; + if (base_name && base_name[0]) { + snprintf(data->name, sizeof(data->name), "%s (%.1fhz)", base_name, hz); + } else { + snprintf(data->name, sizeof(data->name), "%.1fhz", hz); + } +} + +// returns rate index for code, or 0 if autofire is disabled/unset. +int get_autofire_code_idx(int player, uint32_t code) { + for (int i = 0; i != autofiretable[player].count; i++) + { + if (autofiretable[player].autofirecodes[i] == code) + return autofiretable[player].index[i]; + } + return 0; +} + +// autofire structs are private to this unit, so we offer a helper to clear them +void clear_autofire(int player) { + memset(&autofiretable[player], 0, sizeof(AutofireTable)); +} + +// set autofire index for a code: >0 enable, 0 disable. +void set_autofire_code(int player, uint32_t code, uint32_t mask, int index) { + for (int i = 0; i != autofiretable[player].count; i++) { + if (autofiretable[player].autofirecodes[i] == code) { + autofiretable[player].index[i] = index; + autofiretable[player].mask[i] = mask; + return; + } + } + // TODO implement compactification if we run out of slots. + // presently if user enables/disables too many codes we just stop adding new ones. + // represented by MAX_AF_CODES. + if (autofiretable[player].count < MAX_AF_CODES) { + int idx = autofiretable[player].count++; + autofiretable[player].autofirecodes[idx] = code; + autofiretable[player].index[idx] = index; + autofiretable[player].mask[idx] = mask; + } +} + +// step autofire rate; wrap to disabled at max. +void inc_autofire_code(int player, uint32_t code, uint32_t mask) { + int index = get_autofire_code_idx(player, code) + 1; + if (index <= 0) index = 1; + if (index >= num_af_rates || index < 0) index = 0; + set_autofire_code(player, code, mask, index); +} + +// advance all autofire patterns (run once per frame) +void autofire_tick() { + for (int i = 1; i < num_af_rates; i++) + { + autofiredata[i].bit = (autofiredata[i].cycle_mask >> autofiredata[i].frame_count) & 1u; + if (++(autofiredata[i].frame_count) >= autofiredata[i].cycle_length) + autofiredata[i].frame_count = 0; + } +} + +// returns whether the buttons for this code should be held or released this frame. +// (updated every time we call autofire_tick) +bool get_autofire_bit(int player, uint32_t code) { + int rate_idx = get_autofire_code_idx(player, code); + if (rate_idx > 0) { + return autofiredata[rate_idx].bit; + } + return false; +} + +// display-only rate lookup for ui. +const char *get_autofire_rate_hz(int player, uint32_t code) { + int rate_idx = get_autofire_code_idx(player, code); + if (rate_idx <= 0) { + return "disabled"; + } + if (autofiredata[rate_idx].name[0]) { + return autofiredata[rate_idx].name; + } + return "disabled"; +} + +bool is_autofire_enabled(int player, uint32_t code) { + return get_autofire_code_idx(player, code) > 0; +} + +/* autofire configuration parsing/loading */ + +// we accept strings as input so of course this code is about +// 500x longer and more complicated than the actual autofire code + +// autofire config parsing lives here to keep cfg.cpp simple. +// accepts comma-separated float rates, or 0b patterns for custom cycles. +// example: "5.0,10.0,0b11001100,15.0". +// extremely long custom patterns could be used to simulate hold/release + +// some arcade shooters have pretty odd optimal autofire patterns to manage rank +// let's hide them here for my shmup buddies +static const struct AutofireData autofire_patterns[] = { + { "GUNFRONTIER", 0b111100000ULL, 9, 0, 0 }, + { "GAREGGA", 0b1110000ULL, 7, 0, 0 }, +}; + +// helper for formatting binary literal patterns. +static inline const char *bits_to_str(uint64_t value, int cycle_length) { + static char buf[65]; // 64 bits + null + int pos = 0; + int max_len = cycle_length; + if (max_len < 0) max_len = 0; + if (max_len > 64) max_len = 64; + + for (int i = max_len - 1; i >= 0; i--) + buf[pos++] = (value & (1ULL << i)) ? '1' : '0'; + + buf[pos] = '\0'; + return buf; +} + +// slot 0 is always "autofire disabled" initialize defensively +static void init_autofire_entry(struct AutofireData *data, uint64_t mask, int length, + const char *name) { + if (name) { + snprintf(data->name, sizeof(data->name), "%s", name); + } else { + data->name[0] = '\0'; + } + data->cycle_length = length; + data->cycle_mask = mask; + if (data->cycle_length > 64) data->cycle_length = 64; + if (data->cycle_length < 1) data->cycle_length = 1; + data->frame_count = 0; + data->bit = (data->cycle_mask & 1ULL) ? 1 : 0; +} + +static inline struct AutofireData mask_from_hertz(double hz_target) +{ + struct AutofireData p = {{0}, 0, 0, 0, 0}; + + if (hz_target <= 0.0) return p; + if (hz_target > 30.0) hz_target = 30.0; + + int P = (int)ceil(60.0 / hz_target); + if (P < 2) P = 2; + if (P > 64) P = 64; + + p.cycle_length = P; + + int W = P / 2; + if (W < 1) W = 1; + if (W >= P) W = P - 1; + + if (W == 64) + p.cycle_mask = ~0ull; + else + p.cycle_mask = (1ull << W) - 1ull; + + return p; +} + +static void init_default_autofire_data() { + const float default_rates[] = { 10.0f, 15.0f, 30.0f }; + int count = 0; + + init_autofire_entry(&autofiredata_default[count++], 1, 1, NULL); + + for (size_t i = 0; i < (sizeof(default_rates) / sizeof(default_rates[0])); i++) { + if (count >= MAX_AF_RATES) break; + struct AutofireData p = mask_from_hertz(default_rates[i]); + init_autofire_entry(&autofiredata_default[count], p.cycle_mask, + p.cycle_length ? p.cycle_length : 1, NULL); + set_autofire_name(&autofiredata_default[count], NULL); + count++; + } + num_af_rates_default = count; +} + +// parse a 0b... binary pattern; every digit is a frame, up to 64 +// some interesting stuff could be done with longer patterns (hold/release) +static bool parse_autofire_literal(const char *token, uint64_t *mask_out, int *len_out) { + if (!token || token[0] != '0' || !token[1]) return false; + + const char *p = token + 2; + uint64_t mask = 0; + int len = 0; + + if (token[1] != 'b' && token[1] != 'B') return false; + if (!*p) return false; + + for (const char *c = p; *c; c++) { + if (*c != '0' && *c != '1') return false; + if (len >= 64) return false; + mask = (mask << 1) | (uint64_t)(*c - '0'); + len++; + } + + *mask_out = mask; + *len_out = len; + return true; +} + +static void build_autofire_data(const char *rates, struct AutofireData *out, int *out_count) { + char *token; + int count = 0; + char cfg_string[256] = {}; + + init_autofire_entry(&out[count++], 1, 1, NULL); + + snprintf(cfg_string, sizeof(cfg_string), "%s", rates); + + token = strtok(cfg_string, ","); + while (token && count < MAX_AF_RATES) { + bool handled = false; + for (size_t i = 0; i < (sizeof(autofire_patterns) / sizeof(autofire_patterns[0])); i++) { + if (!strcasecmp(token, autofire_patterns[i].name)) { + init_autofire_entry(&out[count], autofire_patterns[i].cycle_mask, + autofire_patterns[i].cycle_length, autofire_patterns[i].name); + set_autofire_name(&out[count], autofire_patterns[i].name); + count++; + handled = true; + break; + } + } + if (handled) { + token = strtok(NULL, ","); + continue; + } + uint64_t literal_mask = 0; + int literal_len = 0; + if (parse_autofire_literal(token, &literal_mask, &literal_len)) { + // use literal directly (allows arbitrary press/release cycles) + init_autofire_entry(&out[count], literal_mask, literal_len, "custom"); + set_autofire_name(&out[count], "custom"); + count++; + token = strtok(NULL, ","); + continue; + } + // pass through to float processing + char *endptr = NULL; + float f = strtof(token, &endptr); + + // reject 0.0 and values > 30.0 + if (endptr && endptr != token && *endptr == '\0' && f > 0.0f && f <= 30.0f) { + struct AutofireData p = mask_from_hertz(f); + init_autofire_entry(&out[count], p.cycle_mask, + p.cycle_length ? p.cycle_length : 1, NULL); + set_autofire_name(&out[count], NULL); + count++; + } + token = strtok(NULL, ","); + } + *out_count = count; +} + +// parse config; fall back to defaults if no valid rates remain. +// always returns true to indicate some rate set is loaded. +bool parse_autofire_cfg() { + printf("[AUTOFIRE INITIALIZATION]\n"); + static bool default_ready = false; + if (!default_ready) { + init_default_autofire_data(); + default_ready = true; + } + + struct AutofireData parsed[MAX_AF_RATES] = {}; + int valid_count = 0; + build_autofire_data(cfg.autofire_rates, parsed, &valid_count); + + if (valid_count > 1) { + memcpy(autofiredata, parsed, sizeof(parsed)); + num_af_rates = valid_count; + } else { + memcpy(autofiredata, autofiredata_default, sizeof(autofiredata_default)); + num_af_rates = num_af_rates_default; + } + + if (valid_count <= 1) { + printf("Autofire configuration in .ini invalid, using default rates:\n"); + } + + printf("Number of autofire rates found: %d\n", num_af_rates - 1); + for (int i = 1; i != num_af_rates; i++) { + printf("%s, bitmask %s, cycle length %u\n", + autofiredata[i].name[0] ? autofiredata[i].name : "custom", + bits_to_str(autofiredata[i].cycle_mask, autofiredata[i].cycle_length), + autofiredata[i].cycle_length); + } + return true; +} diff --git a/autofire.h b/autofire.h new file mode 100644 index 0000000..daa27ba --- /dev/null +++ b/autofire.h @@ -0,0 +1,14 @@ +#ifndef AUTOFIRE_H +#define AUTOFIRE_H + +#include + +const char *get_autofire_rate_hz(int player, uint32_t code); +bool is_autofire_enabled(int player, uint32_t code); +void clear_autofire(int player); +void inc_autofire_code(int player, uint32_t code, uint32_t mask); +void autofire_tick(); +bool parse_autofire_cfg(); +bool get_autofire_bit(int player, uint32_t code); + +#endif diff --git a/cfg.cpp b/cfg.cpp index 1752ecb..9d83f0d 100644 --- a/cfg.cpp +++ b/cfg.cpp @@ -133,6 +133,8 @@ static const ini_var_t ini_vars[] = { "LOOKAHEAD", (void *)(&(cfg.lookahead)), UINT8, 0, 3 }, { "MAIN", (void*)(&(cfg.main)), STRING, 0, sizeof(cfg.main) - 1 }, {"VFILTER_INTERLACE_DEFAULT", (void*)(&(cfg.vfilter_interlace_default)), STRING, 0, sizeof(cfg.vfilter_interlace_default) - 1 }, + { "AUTOFIRE_RATES", (void *)(&(cfg.autofire_rates)), STRING, 0, sizeof(cfg.autofire_rates) - 1 }, + }; static const int nvars = (int)(sizeof(ini_vars) / sizeof(ini_var_t)); @@ -595,6 +597,7 @@ void cfg_parse() has_video_sections = false; using_video_section = false; cfg_error_count = 0; + strcpy(cfg.autofire_rates, "10,15,30"); ini_parse(altcfg(), video_get_core_mode_name(1)); if (has_video_sections && !using_video_section) { diff --git a/cfg.h b/cfg.h index 11906cf..aa824e0 100644 --- a/cfg.h +++ b/cfg.h @@ -101,6 +101,8 @@ typedef struct { uint8_t lookahead; char main[1024]; char vfilter_interlace_default[1023]; + char autofire_rates[256]; + } cfg_t; extern cfg_t cfg; diff --git a/frame_timer.cpp b/frame_timer.cpp new file mode 100644 index 0000000..17531f7 --- /dev/null +++ b/frame_timer.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include + +#include "user_io.h" +#include "frame_timer.h" +#include "video.h" + +// frame timer used by autofire; call frame_timer() periodically and use FRAME_TICK(). +// prefers the core's frame counter, otherwise falls back to timerfd. + +#define SIXTYHERTZ 1668335 // fallback if vtime doesn't work; actually 59.94hz +#define FIFTYHERTZ 2000000 // lowest refresh rate we consider valid 50hz +#define SEVENTYFIVEHERTZ 1326260 // highest refresh rate we consider valid 75.4hz + +uint64_t global_frame_counter = 0; + +extern VideoInfo current_video_info; // from video.cpp +static bool timer_started = false; +static int vtimerfd = -1; + +bool fpga_vsync_timer = false; // flag to indicate core provides frame counter. + +// clamp vrefresh to 50-75Hz; otherwise use 60Hz. +static inline uint32_t get_vtime() { + uint32_t current_vtime = current_video_info.vtime; + // not a bug - a higher refresh rate in hz means a *smaller* vtime value in 10ns units. + if (current_vtime <= FIFTYHERTZ && current_vtime >= SEVENTYFIVEHERTZ) { + return current_vtime; + } + else { + return SIXTYHERTZ; + } +} + +// return true if core vtime changes (resolution or user refresh adjustment). +static inline bool vtime_changed() +{ + static uint32_t prev_vtime; + uint32_t current_vtime = get_vtime(); + if (prev_vtime != current_vtime) { + if (vtimerfd >= 0) { + close(vtimerfd); // recycle timerfd + vtimerfd = -1; + } + printf("frame_timer(): vtime change detected, restarting timer.\n"); + prev_vtime = current_vtime; + return true; + } + return false; +} + +// initialize timerfd-based timer; returns 1 on failure. +int start_vtimer(uint64_t interval_ns) { + vtimerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); + if (vtimerfd < 0) { + perror("timerfd_create"); + return 1; + }; + + // start timer at absolute CLOCK_MONOTONIC time. + // not to be confused with absolute batman. + struct timespec now_ts = {}; + struct itimerspec its = {}; + + clock_gettime(CLOCK_MONOTONIC, &now_ts); + + uint64_t now_ns = (uint64_t)now_ts.tv_sec * 1000000000ULL + now_ts.tv_nsec; + uint64_t first_expiration_ns = now_ns + interval_ns; + + its.it_value.tv_sec = first_expiration_ns / 1000000000ULL; + its.it_value.tv_nsec = first_expiration_ns % 1000000000ULL; + + its.it_interval.tv_sec = interval_ns / 1000000000ULL; + its.it_interval.tv_nsec = interval_ns % 1000000000ULL; + + if (timerfd_settime(vtimerfd, TFD_TIMER_ABSTIME, &its, NULL) < 0) { + close(vtimerfd); + vtimerfd = -1; + printf("frame_timer(): timerfd setup failed, will retry.\n"); + return 1; + } + float hz = 1e9f / interval_ns; + printf("frame_timer(): core does not offer framecounter. using timerfd.\n"); + printf("%.2fhz timer started.\n", hz); + return 0; +} + +// attempt timerfd setup; returns false on failure or zero interval. +static inline bool init_frame_timer(uint32_t interval) +{ + uint64_t interval_ns = interval * 10ull; + if (!timer_started && interval_ns) { + if (start_vtimer(interval_ns) == 0) + return true; + else + return false; + } + return false; +} + +// return true if timer has expired, false otherwise. +static bool check_vtimer() { + uint64_t expirations = 0; + struct pollfd pfd = { vtimerfd, POLLIN, 0 }; + + if (poll(&pfd, 1, 0) <= 0) + return false; // timer not ready + + ssize_t n = read(vtimerfd, &expirations, sizeof(expirations)); + if (n != (ssize_t)sizeof(expirations) || expirations == 0) + return false; + + return true; +} + +// prefer core framecounter; fallback to timerfd with minor long-term drift risk. +// call periodically (e.g., start of input_poll()). +void frame_timer() { + // if core offers its own framecounter skip all the timerfd nonsense + uint32_t frcnt = spi_uio_cmd(UIO_GET_FR_CNT); + if (frcnt & 0x100) { + if (!fpga_vsync_timer) printf("frame_timer(): core offers framecounter.\n"); + fpga_vsync_timer = true; + global_frame_counter = frcnt & 0xFF; + } + else { // fallback to timerfd + if (vtime_changed()) + timer_started = false; // restart timers if vtime has changed; + uint32_t vtime = get_vtime(); + if (!timer_started) + timer_started = init_frame_timer(vtime); + if (timer_started && check_vtimer()) + global_frame_counter++; + } +} diff --git a/frame_timer.h b/frame_timer.h new file mode 100644 index 0000000..bd97fed --- /dev/null +++ b/frame_timer.h @@ -0,0 +1,18 @@ +#ifndef FRAME_TIMER_H +#define FRAME_TIMER_H + +#include + +// macro to tell if a new vertical refresh has happened since the last time we checked +// requires a local uint64_t to track frame counter +#define FRAME_TICK(last) \ + ((global_frame_counter != (last)) ? ((last) = global_frame_counter, 1) : 0) + +void frame_timer(); + +// global +extern uint64_t global_frame_counter; // used by FRAME_TICK() +extern bool fpga_vsync_timer; // does this core expose the frame counter directly? + // exposed globally in case timerfd isn't accurate enough for some use cases + +#endif diff --git a/input.cpp b/input.cpp index dc08225..9646716 100644 --- a/input.cpp +++ b/input.cpp @@ -9,16 +9,17 @@ #include #include #include +#include #include #include #include #include -#include -#include +#include #include #include #include "input.h" +#include "autofire.h" #include "user_io.h" #include "menu.h" #include "hardware.h" @@ -32,9 +33,9 @@ #include "profiling.h" #include "gamecontroller_db.h" #include "str_util.h" +#include "frame_timer.h" #define NUMDEV 30 -#define NUMPLAYERS 6 #define UINPUT_NAME "MiSTer virtual input" char joy_bnames[NUMBUTTONS][32] = {}; @@ -1645,11 +1646,6 @@ static int keyrah_trans(int key, int press) static void input_cb(struct input_event *ev, struct input_absinfo *absinfo, int dev); static int kbd_toggle = 0; -static uint64_t joy[NUMPLAYERS] = {}; // 0-31 primary mappings, 32-64 alternate -static uint64_t autofire[NUMPLAYERS] = {}; // 0-31 primary mappings, 32-64 alternate -static uint32_t autofirecodes[NUMPLAYERS][BTN_NUM] = {}; -static int af_delay[NUMPLAYERS] = {}; - static uint32_t crtgun_timeout[NUMDEV] = {}; static unsigned char mouse_btn = 0; //emulated mouse @@ -1669,10 +1665,6 @@ static uint32_t mouse_timer = 0; #define BTN_TGL 100 #define BTN_OSD 101 -#define AF_MIN 16 -#define AF_MAX 512 -#define AF_STEP 8 - static int uinp_fd = -1; static int input_uinp_setup() { @@ -1841,98 +1833,158 @@ static void joy_apply_deadzone(int* x, int* y, const devInput* dev, const int st joy_clamp(y, min_range, INT8_MAX); } -static uint32_t osdbtn = 0; -static void joy_digital(int jnum, uint64_t mask, uint32_t code, char press, int bnum, int dont_save = 0) +static bool osd_autofire_consumed[NUMPLAYERS] = {}; + +// returns true if autofire was toggled which also means input was consumed. +static bool handle_autofire_toggle(int num, uint32_t mask, uint32_t code, char press, int bnum, int dont_save) { - static char str[128]; static uint32_t lastcode[NUMPLAYERS]; - static uint64_t lastmask[NUMPLAYERS]; - int num = jnum - 1; - if (num < NUMPLAYERS) + static uint32_t lastmask[NUMPLAYERS]; + static char str[512]; + + // if the button is not OSD or BTN_TGL we save it into lastmask/lastcode + if (bnum != BTN_OSD && bnum != BTN_TGL) { - if (jnum) + if (!dont_save) { - if (bnum != BTN_OSD && bnum != BTN_TGL) + if (press) { - if (!dont_save) - { - if (press) - { - lastcode[num] = code; - lastmask[num] = mask; - } - else - { - lastcode[num] = 0; - lastmask[num] = 0; - } + if (lastcode[num] == code) { // build up mask if multiple buttons map to same code + lastmask[num] |= mask; // this can't happen at present but if it ever does it will work correctly + + } else { + lastcode[num] = code; + lastmask[num] = mask; } } else { - if (!user_io_osd_is_visible() && press && !cfg.disable_autofire) - { - if (lastcode[num] && lastmask[num]) - { - int found = 0; - int zero = -1; - for (uint i = 0; i < BTN_NUM; i++) - { - if (!autofirecodes[num][i]) zero = i; - if (autofirecodes[num][i] == lastcode[num]) - { - found = 1; - autofirecodes[num][i] = 0; - break; - } - } - - if (!found && zero >= 0) autofirecodes[num][zero] = lastcode[num]; - - autofire[num] = !found ? autofire[num] | lastmask[num] : autofire[num] & ~lastmask[num]; - - if (hasAPI1_5()) - { - if (!found) sprintf(str, "Auto fire: %dms (%uhz)", af_delay[num] * 2, 1000 / (af_delay[num] * 2)); - else sprintf(str, "Auto fire: OFF"); - Info(str); - } - else InfoMessage((!found) ? "\n\n Auto fire\n ON" : - "\n\n Auto fire\n OFF"); - - return; - } - else if (lastmask[num] & 0xF) - { - if (lastmask[num] & 9) - { - af_delay[num] += AF_STEP << ((lastmask[num] & 1) ? 1 : 0); - if (af_delay[num] > AF_MAX) af_delay[num] = AF_MAX; - } - else - { - af_delay[num] -= AF_STEP << ((lastmask[num] & 2) ? 1 : 0); - if (af_delay[num] < AF_MIN) af_delay[num] = AF_MIN; - } - - static char str[256]; - - if (hasAPI1_5()) - { - sprintf(str, "Auto fire period: %dms (%uhz)", af_delay[num] * 2, 1000 / (af_delay[num] * 2)); - Info(str); - } - else - { - sprintf(str, "\n\n Auto fire period\n %dms(%uhz)", af_delay[num] * 2, 1000 / (af_delay[num] * 2)); - InfoMessage(str); - } - - return; - } - } + lastcode[num] = 0; + lastmask[num] = 0; } } + return false; + } + + // we can only get here if the OSD or BTN_TGL keys were pressed + // in that event we see if lastmask/lastcode tells us we're holding a button + // and if we are, we toggle autofire for that button + if (!user_io_osd_is_visible() && press && !cfg.disable_autofire) + { + if ((lastcode[num] && lastmask[num] && (lastmask[num] & 0xF) == 0)) // don't allow enabling autofire on directions + { + char *strat = str; + inc_autofire_code(num, lastcode[num], lastmask[num]); + + // display autofire status for each button in the mask + FOR_EACH_SET_BIT(lastmask[num], btn) { + strat += sprintf(strat, "%s\n", joy_bnames[btn-4]); + } + + const char *rate = get_autofire_rate_hz(num, lastcode[num]); + + if (!strcmp(rate, "disabled")) { + strat += sprintf(strat, "Autofire disabled"); + } else { + strat += sprintf(strat, "Autofire: %s", rate); + } + + if (hasAPI1_5()) + Info(str); + else + InfoMessage(str); + if (bnum == BTN_OSD && press) osd_autofire_consumed[num] = true; + return true; + } + } + return false; +} + +static uint32_t osdbtn = 0; + +struct KeyStates { + uint32_t key[256]; + uint32_t mask[256]; + int count; +}; + +#define MAX_KEY_STATES 64 + +KeyStates key_states[NUMPLAYERS] = {}; + +static void set_key_state(int player, uint32_t key, bool press, uint32_t mask) +{ + for (int i = 0; i < key_states[player].count; i++) + { + if (key_states[player].key[i] == key) + { + if (press) + { + key_states[player].mask[i] |= mask; + return; + } + else + { + key_states[player].mask[i] &= ~mask; + // remove an entry if no bits are set. + if (!key_states[player].mask[i]) { + int last = key_states[player].count - 1; + if (i != last) { + key_states[player].key[i] = key_states[player].key[last]; + key_states[player].mask[i] = key_states[player].mask[last]; + } + key_states[player].key[last] = 0; + key_states[player].mask[last] = 0u; + key_states[player].count--; + } + return; + } + } + } + if (key_states[player].count < MAX_KEY_STATES) + { + if (press) + { + int idx = key_states[player].count++; + key_states[player].key[idx] = key; + key_states[player].mask[idx] = mask; + } + return; + } +} + +uint32_t build_joy_mask(int player) +{ + uint32_t mask = 0u; + for (int i = 0; i < key_states[player].count; i++) + { + if (!is_autofire_enabled(player, key_states[player].key[i])) + mask |= key_states[player].mask[i]; + } + return mask; +} + +uint32_t build_autofire_mask(int player) +{ + uint32_t mask = 0u; + for (int i = 0; i < key_states[player].count; i++) + { + if (is_autofire_enabled(player, key_states[player].key[i])) + if (get_autofire_bit(player, key_states[player].key[i])) + mask |= key_states[player].mask[i]; + } + return mask; +} + +static void joy_digital(int jnum, uint32_t mask, uint32_t code, char press, int bnum, int dont_save = 0) +{ + int num = jnum - 1; + if (num < NUMPLAYERS) + { + // autofire handler moved to helper function for clarity + if (handle_autofire_toggle(num, mask, code, press, bnum, dont_save)) { + return; + } if (bnum == BTN_TGL) { @@ -1960,6 +2012,13 @@ static void joy_digital(int jnum, uint64_t mask, uint32_t code, char press, int return; } + // toggling autofire via OSD button consumes the "press" event of OSD but not the release event + // this suppresses that so toggling autofire on a button doesn't immediately disable the button + if (bnum == BTN_OSD && !press && osd_autofire_consumed[num]) { + osd_autofire_consumed[num] = false; + return; + } + // clear OSD button state if not in the OSD. this avoids problems where buttons are still held // on OSD exit and causes combinations to match when partial buttons are pressed. if (!user_io_osd_is_visible()) osdbtn = 0; @@ -1998,13 +2057,13 @@ static void joy_digital(int jnum, uint64_t mask, uint32_t code, char press, int if((mask & JOY_BTN2) && !(old_osdbtn & JOY_BTN2)) mask = 0; } - memset(joy, 0, sizeof(joy)); + memset(key_states, 0, sizeof(key_states)); struct input_event ev; ev.type = EV_KEY; ev.value = press; int cfg_switch = menu_allow_cfg_switch() && (osdbtn & JOY_BTN2) && press; - + switch (mask) { case JOY_RIGHT: @@ -2128,17 +2187,7 @@ static void joy_digital(int jnum, uint64_t mask, uint32_t code, char press, int } else if(jnum) { - if (press) joy[num] |= mask; - else joy[num] &= ~mask; - - //user_io_digital_joystick(num, joy[num]); - - if (code) - { - int found = 0; - for (uint i = 0; i < BTN_NUM; i++) if (autofirecodes[num][i] == code) found = 1; - if (found) autofire[num] = press ? autofire[num] | mask : autofire[num] & ~mask; - } + set_key_state(num, code, press, mask); } } } @@ -2342,6 +2391,11 @@ void reset_players() input[i].map_shown = 0; update_num_hw(i, 0); } + + memset(key_states, 0, sizeof(key_states)); + for (int i = 0; i < NUMPLAYERS; i++) { + clear_autofire(i); + } memset(player_pad, 0, sizeof(player_pad)); memset(player_pdsp, 0, sizeof(player_pdsp)); } @@ -3301,13 +3355,8 @@ static void input_cb(struct input_event *ev, struct input_absinfo *absinfo, int for (uint i = 0; i < BTN_NUM; i++) { - uint64_t mask = 0; - if (ev->code == (input[dev].map[i] & 0xFFFF)) mask = (uint64_t)1 << i; - else if (ev->code == (input[dev].map[i] >> 16)) mask = (uint64_t)1 << (i + 32); // 1 is uint32_t. i spent hours realizing this. - if (mask) { - if (i <= 3 && origcode == ev->code) origcode = 0; // prevent autofire for original dpad - if (ev->value <=1) joy_digital(input[dev].num, mask, origcode, ev->value, i, (ev->code == input[dev].mmap[SYS_BTN_OSD_KTGL + 1] || ev->code == input[dev].mmap[SYS_BTN_OSD_KTGL + 2])); - // support 2 simultaneous functions for 1 button if defined in 2 sets. No return. + if (ev->code == (input[dev].map[i] & 0xFFFF) || ev->code == (input[dev].map[i] >> 16)) { + if (ev->value <= 1) joy_digital(input[dev].num, 1 << i, origcode, ev->value, i, (ev->code == input[dev].mmap[SYS_BTN_OSD_KTGL + 1] || ev->code == input[dev].mmap[SYS_BTN_OSD_KTGL + 2])); } } @@ -3359,11 +3408,15 @@ static void input_cb(struct input_event *ev, struct input_absinfo *absinfo, int { if (ev->code == (uint16_t)input[dev].map[i]) { - if (i <= 3 && origcode == ev->code) origcode = 0; // prevent autofire for original dpad if (ev->value <= 1) joy_digital((user_io_get_kbdemu() == EMU_JOY0) ? 1 : 2, 1 << i, origcode, ev->value, i); return; } } + // if we move the return above to here, it should change behavior so that joy_digital() is called once for every + // button that is mapped to a given code. each call would have the same ev->code but a different mask and bnum. + // no time to test now though so leaving as note to anybody else who wants to try it. current system can't even + // generate an ev->code with multiple buttons + // return; } if (ev->code == input[dev].mmap[SYS_BTN_OSD_KTGL]) @@ -4699,6 +4752,12 @@ int input_test(int getchar) pool[i].events = 0; } + // clear button reference counts and key states + memset(key_states, 0, sizeof(key_states)); + for (int i = 0; i < NUMPLAYERS; i++) { + clear_autofire(i); + } + memset(input, 0, sizeof(input)); int n = 0; @@ -5787,10 +5846,15 @@ int input_test(int getchar) int input_poll(int getchar) { PROFILE_FUNCTION(); - - static int af[NUMPLAYERS] = {}; - static uint32_t time[NUMPLAYERS] = {}; - static uint64_t joy_prev[NUMPLAYERS] = {}; + static bool autofire_cfg_parsed = false; + if (!autofire_cfg_parsed) autofire_cfg_parsed = parse_autofire_cfg(); + static uint32_t joy_mask_prev[NUMPLAYERS] = {}; + + // FRAME_TICK compares against frame_timer's counter (updated elsewhere) and fires once per frame. + static uint32_t last_frame_count = 0; + if (FRAME_TICK(last_frame_count)) { + autofire_tick(); // advance all autofire patterns by 1 + } int ret = input_test(getchar); if (getchar) return ret; @@ -5824,41 +5888,23 @@ int input_poll(int getchar) if (!mouse_emu_x && !mouse_emu_y) mouse_timer = 0; + uint32_t joy_mask[NUMPLAYERS]; + uint32_t autofire_mask[NUMPLAYERS]; + + for (int i = 0; i < NUMPLAYERS; i++) { + joy_mask[i] = build_joy_mask(i); + autofire_mask[i] = build_autofire_mask(i); + } + if (grabbed) { - for (int i = 0; i < NUMPLAYERS; i++) - { - int send = 0; - if (af_delay[i] < AF_MIN) af_delay[i] = AF_MIN; - - /* Autofire handler */ - if (joy[i] & autofire[i]) + for (int i = 0; i < NUMPLAYERS; i++) { + joy_mask[i] = joy_mask[i] | autofire_mask[i]; + int newdir = (joy_mask[i] & 0xF) | (joy_mask_prev[i] & 0xF); + if (joy_mask[i] != joy_mask_prev[i]) { - if (!time[i]) time[i] = GetTimer(af_delay[i]); - else if ((joy[i] ^ joy_prev[i]) & autofire[i]) - { - time[i] = GetTimer(af_delay[i]); - af[i] = 0; - } - else if (CheckTimer(time[i])) - { - time[i] = GetTimer(af_delay[i]); - af[i] = !af[i]; - send = 1; - } - } - - int newdir = ((((uint32_t)(joy[i]) | (uint32_t)(joy[i] >> 32)) & 0xF) != (((uint32_t)(joy_prev[i]) | (uint32_t)(joy_prev[i] >> 32)) & 0xF)); - - if (joy[i] != joy_prev[i]) - { - joy_prev[i] = joy[i]; - send = 1; - } - - if (send) - { - user_io_digital_joystick(i, af[i] ? joy[i] & ~autofire[i] : joy[i], newdir); + joy_mask_prev[i] = joy_mask[i]; + user_io_digital_joystick(i, joy_mask[i], newdir); } } } @@ -5867,12 +5913,9 @@ int input_poll(int getchar) { for (int i = 0; i < NUMPLAYERS; i++) { - if(joy[i]) user_io_digital_joystick(i, 0, 1); - - joy[i] = 0; - af[i] = 0; - autofire[i] = 0; + if(joy_mask[i]) user_io_digital_joystick(i, 0, 1); } + memset(key_states, 0, sizeof(key_states)); } if (mouse_req) @@ -5895,7 +5938,7 @@ int input_poll(int getchar) int is_key_pressed(int key) { - unsigned char bits[(KEY_MAX + 7) / 8]; + unsigned char bits[(KEY_CNT + 7) / 8]; for (int i = 0; i < NUMDEV; i++) { if (pool[i].fd > 0) diff --git a/input.h b/input.h index 892cbf1..8aa7429 100644 --- a/input.h +++ b/input.h @@ -29,6 +29,8 @@ #define UPSTROKE 0x400000 +#define NUMPLAYERS 6 + #define NUMBUTTONS 32 #define BUTTON_DPAD_COUNT 12 // dpad + 8 buttons @@ -114,4 +116,8 @@ void parse_buttons(); char *get_buttons(int type = 0); void set_ovr_buttons(char *s, int type); +#define FOR_EACH_SET_BIT(mask, bit) \ + for (uint32_t _m = (mask); _m; _m &= (_m - 1)) \ + for (int bit = __builtin_ctz(_m), _once = 1; _once; _once = 0) + #endif diff --git a/main.cpp b/main.cpp index 99c03eb..f1eaf82 100644 --- a/main.cpp +++ b/main.cpp @@ -29,6 +29,7 @@ along with this program. If not, see . #include "menu.h" #include "user_io.h" #include "input.h" +#include "frame_timer.h" #include "fpga_io.h" #include "scheduler.h" #include "osd.h" @@ -83,6 +84,7 @@ int main(int argc, char *argv[]) } user_io_poll(); + frame_timer(); input_poll(0); HandleUI(); OsdUpdate(); diff --git a/scheduler.cpp b/scheduler.cpp index 919d31a..afa29b2 100644 --- a/scheduler.cpp +++ b/scheduler.cpp @@ -4,6 +4,7 @@ #include "menu.h" #include "user_io.h" #include "input.h" +#include "frame_timer.h" #include "fpga_io.h" #include "osd.h" #include "profiling.h" @@ -30,6 +31,7 @@ static void scheduler_co_poll(void) { SPIKE_SCOPE("co_poll", 1000); user_io_poll(); + frame_timer(); input_poll(0); } diff --git a/user_io.cpp b/user_io.cpp index 4f108fe..3ab151f 100644 --- a/user_io.cpp +++ b/user_io.cpp @@ -1735,23 +1735,21 @@ void user_io_r_analog_joystick(unsigned char joystick, char valueX, char valueY) } } -void user_io_digital_joystick(unsigned char joystick, uint64_t map, int newdir) +void user_io_digital_joystick(unsigned char joystick, uint32_t map, int newdir) { uint8_t joy = (joystick>1 || !joyswap) ? joystick : joystick ^ 1; + static int use32 = 0; - // primary button mappings are in 31:0, alternate mappings are in 64:32. - // take the logical OR to ensure a held button isn't overriden - // by other mapping being pressed - uint32_t bitmask = (uint32_t)(map) | (uint32_t)(map >> 32); - use32 |= bitmask >> 16; + use32 |= map >> 16; + spi_uio_cmd_cont((joy < 2) ? (UIO_JOYSTICK0 + joy) : (UIO_JOYSTICK2 + joy - 2)); - spi_w(bitmask); - if(use32) spi_w(bitmask >> 16); + spi_w(map); + if(use32) spi_w(map >> 16); DisableIO(); if (!is_minimig() && joy_transl == 1 && newdir) { - user_io_l_analog_joystick(joystick, (bitmask & 2) ? 128 : (bitmask & 1) ? 127 : 0, (bitmask & 8) ? 128 : (bitmask & 4) ? 127 : 0); + user_io_l_analog_joystick(joystick, (map & 2) ? 128 : (map & 1) ? 127 : 0, (map & 8) ? 128 : (map & 4) ? 127 : 0); } } diff --git a/user_io.h b/user_io.h index 69f04d0..5c31026 100644 --- a/user_io.h +++ b/user_io.h @@ -206,7 +206,7 @@ void user_io_mouse(unsigned char b, int16_t x, int16_t y, int16_t w); void user_io_kbd(uint16_t key, int press); char* user_io_create_config_name(int with_ver = 0); int user_io_get_joy_transl(); -void user_io_digital_joystick(unsigned char, uint64_t, int); +void user_io_digital_joystick(unsigned char, uint32_t, int); void user_io_l_analog_joystick(unsigned char, char, char); void user_io_r_analog_joystick(unsigned char, char, char); void user_io_set_joyswap(int swap); diff --git a/video.cpp b/video.cpp index 908ed89..8e4b3f0 100644 --- a/video.cpp +++ b/video.cpp @@ -75,7 +75,7 @@ static int brd_y = 0; static int menu_bg = 0; static int menu_bgn = 0; -static VideoInfo current_video_info; +VideoInfo current_video_info; static int support_FHD = 0; diff --git a/video.h b/video.h index e8e7cb7..6225a80 100644 --- a/video.h +++ b/video.h @@ -30,6 +30,9 @@ struct VideoInfo bool rotated; }; +// expose video timings for timerfd-based frame timer +extern VideoInfo current_video_info; + void video_init(); int video_get_scaler_flt(int type);