Make the Fuller Box optional and add Melodik, both defaulting off

src/fuse/settings.c hardcoded settings_current.fuller = 1 in the replacement
settings_init(), overriding Fuse's own default of 0 (fuse/settings.c:126). The
Fuller Box is one periph_t covering an AY-3-8912 at ports 0x3f/0x5f and a
joystick at 0x7f (fuse/peripherals/fuller.c:51-55), so that left the AY
permanently decoded on every machine, including plain 48K. Titles that poke
those ports on the off chance - Vixen, Andy Capp and others - therefore played
AY music that no unexpanded Spectrum could have produced.

Drive it from a fuse_fuller_box core option instead, defaulting to disabled to
match real hardware, and add fuse_melodik alongside it: Melodik is the other
way a 48K gets an AY, decoding the 128K ports (0xfffd/0xbffd), and it is the
same one-line plumbing job.

Three things fall out of this:

- The Fuller joystick is part of the same periph_t, so leaving the box off
  would silently break RETRO_DEVICE_FULLER_JOYSTICK. Generalise
  sync_kempston_mouse_from_ports() into sync_periph_from_ports_and_options()
  and OR the option with "any port is wired to a Fuller joystick", the mirror
  image of what that function already does for the Kempston Mouse.

- Both peripherals declare hard_reset, so the deferred update at the top of
  retro_run() becomes periph_posthook() rather than periph_update().
  periph_posthook() only resets when a peripheral that needs it actually
  changed, so the Kempston Mouse path through there is unaffected.

- fuller_enabled_snapshot() and melodik_enabled_snapshot() re-derive both
  settings from a loaded snapshot, exactly as kempmouse_snapshot_enabled()
  does. The existing re-sync call sites in retro_load_game(),
  retro_unserialize() and the snapshot content path now cover all three.

Note that this is a user-visible change for anyone who has been getting AY
music out of 48K content: they were getting it by accident, but they will
notice it stop.

Verified against the real port decoding with Fuse's own readport()/writeport()
on a 48K machine rather than by inspecting settings_current: 0x3f reads back
0xff by default and the written AY register once enabled, 0xfffd likewise for
Melodik, wiring and unwiring a Fuller joystick toggles the box, and a state
captured with the box on does not re-enable it on restore.
This commit is contained in:
Lib authored and libretroadmin committed 2026-08-05 21:52:10 +02:00
1 parent e9326116ab
commit 3c725a7c61
2 files changed
+81 -25

No files matched your search

+5 -1
View File
@@ -24,7 +24,11 @@ int settings_init(int *first_arg, int argc, char **argv)
settings_current.sound_load = 1;
settings_current.joy_kempston = 1;
settings_current.fuller = 1;
/* settings_current.fuller is deliberately left at Fuse's default of 0 and
driven by the fuse_fuller_box core option from update_variables() below.
Forcing it on here left the Fuller Box AY permanently decoded at ports
0x3f/0x5f on every machine, so plain 48K titles that poke those ports
(Vixen, Andy Capp, ...) got AY music no real 48K would have produced. */
#ifndef __LIBRETRO__
settings_current.joystick_1_output = 1;
settings_current.joystick_2_output = 2;
+76 -24
View File
@@ -240,11 +240,19 @@ static int show_joystick_type_at_startup;
static int show_emulation_speed_at_startup;
static int display_joystick_type;
static int display_emulation_speed;
static int kempston_mouse_needs_periph_update = 0;
static void sync_kempston_mouse_from_ports(void);
static int periph_needs_update = 0;
static void sync_periph_from_ports_and_options(void);
static int sound_needs_reinit = 0;
/* Mirrors of the user's current selections for the peripheral options. These
have to be kept separately from settings_current because Fuse re-derives
the corresponding settings from whatever a loaded snapshot happened to
record (fuller_enabled_snapshot(), melodik_enabled_snapshot()), and the
user's option is authoritative over that. */
static int opt_fuller = 0;
static int opt_melodik = 0;
/* Mirrors of the user's current selections for the options that sound_init()
reads, so update_variables() can tell a real change from a re-read.
-1 means "not read yet", so the first pass never looks like a change. */
@@ -696,6 +704,26 @@ static struct retro_core_option_v2_definition core_option_definitions[] = {
{ CORE_OPTION_VALUE_LIST_ENABLED_DISABLED },
"disabled"
},
{
"fuse_fuller_box",
"Fuller Box",
NULL,
"Emulate the Fuller Box AY-3-8912 audio interface (ports 0x3f/0x5f) and its joystick port. Leave disabled for plain 48K titles: some of them poke those ports and will play AY music that no unexpanded Spectrum could produce. Selecting the Fuller joystick on a controller port enables the box regardless of this setting. Changing this resets the machine.",
NULL,
"audio",
{ CORE_OPTION_VALUE_LIST_ENABLED_DISABLED },
"disabled"
},
{
"fuse_melodik",
"Melodik",
NULL,
"Emulate the Melodik AY-3-8912 audio interface, which decodes the 128K AY ports (0xfffd/0xbffd) on a 48K machine. Changing this resets the machine.",
NULL,
"audio",
{ CORE_OPTION_VALUE_LIST_ENABLED_DISABLED },
"disabled"
},
{
"fuse_volume_ay",
"AY Volume",
@@ -952,6 +980,8 @@ static const struct retro_variable core_vars[] =
{ "fuse_speaker_type", "Speaker Type; tv speaker|beeper|unfiltered" },
{ "fuse_ay_stereo_separation", "AY Stereo Separation; none|acb|abc" },
{ "fuse_turbosound", "TurboSound (2x AY-8910); disabled|enabled" },
{ "fuse_fuller_box", "Fuller Box; disabled|enabled" },
{ "fuse_melodik", "Melodik; disabled|enabled" },
{ "fuse_volume_ay", "AY Volume; " VOLUME_LEVELS },
{ "fuse_volume_beeper", "Beeper Volume; " VOLUME_LEVELS },
{ "fuse_key_ovrlay_transp", "Transparent Keyboard Overlay; enabled|disabled" },
@@ -1225,6 +1255,11 @@ int update_variables(int force)
opt_volume_beeper = volume;
}
opt_fuller = coreopt(env_cb, core_vars, "fuse_fuller_box", NULL) == 1;
opt_melodik = coreopt(env_cb, core_vars, "fuse_melodik", NULL) == 1;
sync_periph_from_ports_and_options();
keyb_transparent = coreopt(env_cb, core_vars, "fuse_key_ovrlay_transp", NULL) != 1;
{
@@ -2050,7 +2085,7 @@ bool retro_load_game(const struct retro_game_info *info)
// still 0); likewise, loading snapshot content re-derives the flag
// from the file via kempmouse_snapshot_enabled(). Port wiring is
// frontend state, not machine state - see retro_unserialize().
sync_kempston_mouse_from_ports();
sync_periph_from_ports_and_options();
return true;
}
@@ -2261,10 +2296,15 @@ void retro_run(void)
{
bool updated = false;
if (kempston_mouse_needs_periph_update)
if (periph_needs_update)
{
kempston_mouse_needs_periph_update = 0;
periph_update();
periph_needs_update = 0;
// periph_posthook() rather than periph_update(): the Fuller Box and
// Melodik both declare hard_reset, and plugging or unplugging a real
// one is not something the running machine survives. periph_posthook()
// only resets when a peripheral that needs it actually changed, so the
// Kempston Mouse path through here still behaves as it did.
periph_posthook();
}
if (sound_needs_reinit)
@@ -2426,38 +2466,50 @@ void retro_deinit(void)
}
}
// Re-derive every Fuse peripheral setting whose authoritative value lives
// outside the emulated machine: live frontend port wiring, and core options.
//
// Kempston Mouse is a single peripheral, not per-port; enable it in Fuse
// whenever any port is currently configured as a mouse, disable it
// otherwise. This reflects live frontend port wiring, not emulated
// machine state, so it must be re-applied after a snapshot/rewind restore
// too - the snapshot's own kempston_mouse_active flag only reflects
// whatever was true at the moment that particular state was captured
// (e.g. still off, if rewound back to before the mouse was ever
// connected), which can otherwise leave the peripheral disabled even
// though the frontend still has a mouse wired to a port right now.
static void sync_kempston_mouse_from_ports(void)
// otherwise. The Fuller Box is the same shape of problem in reverse - its
// joystick port (0x7f) and its AY (0x3f/0x5f) are one periph_t, so wiring a
// Fuller joystick to a port has to switch the whole box on regardless of
// the fuse_fuller_box option.
//
// None of this is emulated machine state, so it must be re-applied after a
// snapshot/rewind restore too: kempmouse_snapshot_enabled(),
// fuller_enabled_snapshot() and melodik_enabled_snapshot() all overwrite the
// corresponding settings_current fields with whatever was true at the moment
// that particular state was captured (e.g. still off, if rewound back to
// before the mouse was ever connected), which can otherwise leave a
// peripheral disabled even though the frontend has it wired up right now.
static void sync_periph_from_ports_and_options(void)
{
unsigned p;
int kempston_mouse = 0;
int fuller = opt_fuller;
for (p = 0; p < MAX_PADS; p++)
{
if (input_devices[p] == RETRO_DEVICE_KEMPSTON_MOUSE)
{
kempston_mouse = 1;
break;
}
else if (input_devices[p] == RETRO_DEVICE_FULLER_JOYSTICK)
fuller = 1;
}
if (settings_current.kempston_mouse != kempston_mouse)
if (settings_current.kempston_mouse != kempston_mouse ||
settings_current.fuller != fuller ||
settings_current.melodik != opt_melodik)
{
settings_current.kempston_mouse = kempston_mouse;
settings_current.fuller = fuller;
settings_current.melodik = opt_melodik;
// See the comment in retro_set_controller_port_device(): defer the
// actual periph_update() to the top of retro_run() rather than
// actual peripheral update to the top of retro_run() rather than
// calling it here synchronously.
if (fuse_init_called)
kempston_mouse_needs_periph_update = 1;
periph_needs_update = 1;
}
}
@@ -2517,7 +2569,7 @@ void retro_set_controller_port_device(unsigned port, unsigned device)
// unscheduled, so retro_run()'s "wait until some_audio" loop spins
// forever and the frontend hangs. Defer to the top of retro_run()
// instead, exactly like UPDATE_MACHINE (model changes) already does.
sync_kempston_mouse_from_ports();
sync_periph_from_ports_and_options();
}
void retro_reset(void)
@@ -2546,7 +2598,7 @@ void retro_reset(void)
machine_reset(1);
}
sync_kempston_mouse_from_ports();
sync_periph_from_ports_and_options();
return;
}
@@ -2565,7 +2617,7 @@ void retro_reset(void)
// snapshot_copy_from() and re-derived settings_current.kempston_mouse
// from the file, exactly like retro_unserialize() - re-apply the live
// controller-port wiring here too.
sync_kempston_mouse_from_ports();
sync_periph_from_ports_and_options();
}
size_t retro_serialize_size(void)
@@ -2670,7 +2722,7 @@ bool retro_unserialize(const void *data, size_t size)
// every restore (rewind or manual load) rather than leaving whatever
// the snapshot happened to say.
if (ok)
sync_kempston_mouse_from_ports();
sync_periph_from_ports_and_options();
return ok;
}