input: autofire overhaul (#1091)

* frame-synchronized autofire, per-button rates, custom rates via mister.ini
* frame timer using core frame counter, fallback to timerfd
* improved button reference counting (previously limited to 2)
This commit is contained in:
Tony Toon
2026-01-21 23:07:44 -06:00
committed by GitHub
parent c397c9413d
commit 4d1a9fc2d3
15 changed files with 743 additions and 167 deletions

View File

@@ -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

342
autofire.cpp Normal file
View File

@@ -0,0 +1,342 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdint.h>
#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;
}

14
autofire.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef AUTOFIRE_H
#define AUTOFIRE_H
#include <stdint.h>
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

View File

@@ -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)
{

2
cfg.h
View File

@@ -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;

139
frame_timer.cpp Normal file
View File

@@ -0,0 +1,139 @@
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include <sys/timerfd.h>
#include <sys/poll.h>
#include <unistd.h>
#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++;
}
}

18
frame_timer.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef FRAME_TIMER_H
#define FRAME_TIMER_H
#include <stdint.h>
// 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

355
input.cpp
View File

@@ -9,16 +9,17 @@
#include <sys/sysinfo.h>
#include <dirent.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <stdarg.h>
#include <math.h>
#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)

View File

@@ -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

View File

@@ -29,6 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#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();

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);