Files
Antonio SJ Musumeci 39bf0700c1 Fix streaming completion timing on CD-ROM reads and CLIO input DMA
Wing Commander III - Heart of the Tiger (USA, Disc 1) could stall during
its credits or opening movies because streaming completion became visible
at the wrong boundaries.

Publish successful MEI READ_DATA status only after the final requested
byte has left the data FIFO. Reporting success as soon as one sector is
buffered prematurely ends Portfolio's read-ahead command, even while data
from the request remains unread.

Raise CLIO input-DMA completion and reload the next buffer when the final
sample is consumed, rather than waiting for another DSP read. Wing
Commander III can otherwise observe the end address and detach an audio
attachment whose completion has not fired. Once the instrument stops,
there is no further read to generate completion and playback waits
forever.

During investigation, stock-2x playback advanced through the previously
stuck credits and opening movie sequence to the interactive hangar. This
addresses the observed streaming stalls, not an exhaustive compatibility
claim for every gameplay or disc-transition path.

Also default CD-ROM speed to the stock 2x rate in both option definitions
and the missing-option fallback. This affects all titles using the
default; explicit unbounded and other saved speed selections remain
available and unchanged.
2026-10-22 21:45:34 -05:00

871 lines
18 KiB
C

/*
ISC License
Copyright (c) 2021, Antonio SJ Musumeci <[email protected]>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "opera_lr_opts.h"
#include "opera_lr_callbacks.h"
#include "opera_lr_dsp.h"
#include "lr_input.h"
#include "libopera/opera_arm.h"
#include "libopera/opera_bios.h"
#include "libopera/opera_cdrom.h"
#include "libopera/opera_clock.h"
#include "libopera/opera_core.h"
#include "libopera/opera_madam.h"
#include "libopera/opera_mem.h"
#include "libopera/opera_region.h"
#include "libopera/opera_vdlp.h"
#include "file/file_path.h"
#include "retro_miscellaneous.h"
#include "streams/file_stream.h"
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
opera_lr_opts_t g_OPTS = {0};
static
int
getvar(struct retro_variable *var_)
{
return retro_environment_cb(RETRO_ENVIRONMENT_GET_VARIABLE,var_);
}
static
const
char*
getval(const char *key_)
{
int rv;
char key[64];
struct retro_variable var;
strncpy(key,"opera_",(sizeof(key)-1));
strncat(key,key_,(sizeof(key)-1));
var.key = key;
var.value = NULL;
rv = getvar(&var);
if(rv && (var.value != NULL))
return var.value;
return NULL;
}
static
int
getval_as_int(const char *key_,
const int default_)
{
const char *val;
val = getval(key_);
if(val == NULL)
return default_;
return atoi(val);
}
static
float
getval_as_float(const char *key_,
const float default_)
{
const char *val;
val = getval(key_);
if(val == NULL)
return default_;
return atof(val);
}
static
bool
getval_is_str(const char *key_,
const char *str_,
const bool default_)
{
const char *val;
val = getval(key_);
if(val == NULL)
return default_;
return (strcmp(val,str_) == 0);
}
static
bool
getval_is_enabled(const char *key_,
const bool default_)
{
return getval_is_str(key_,"enabled",default_);
}
bool
opera_lr_opts_get_random_seed(uint32_t *seed_)
{
int base;
char *end;
const char *val;
unsigned long parsed;
if(seed_ == NULL)
return false;
val = getval("random_seed");
if((val == NULL) || !strcmp(val,"random"))
return false;
base = 10;
if((val[0] == '0') && ((val[1] == 'x') || (val[1] == 'X')))
{
base = 16;
if(!isxdigit((unsigned char)val[2]))
goto invalid;
}
else if(!isdigit((unsigned char)val[0]))
goto invalid;
errno = 0;
end = NULL;
parsed = strtoul(val,&end,base);
if((errno != 0) || (end == val) || (*end != '\0') ||
(parsed > UINT32_MAX))
goto invalid;
*seed_ = (uint32_t)parsed;
return true;
invalid:
retro_log_printf_cb(RETRO_LOG_WARN,
"[Opera]: invalid random seed '%s'; using time-based seed\n",
val);
return false;
}
static
void
opera_lr_opts_get_mem_cfg(opera_lr_opts_t *opts_)
{
const char *val;
opts_->mem_cfg = DRAM_VRAM_STOCK;
val = getval("mem_capacity");
if(val == NULL)
return;
if((strlen(val) == 2) &&
(isxdigit((unsigned char)val[0]) != 0) &&
(isxdigit((unsigned char)val[1]) != 0))
{
opera_mem_cfg_t cfg;
cfg = (opera_mem_cfg_t)strtoul(val,NULL,16);
if(opera_mem_is_valid_cfg(cfg))
{
opts_->mem_cfg = cfg;
return;
}
}
retro_log_printf_cb(RETRO_LOG_WARN,
"[Opera]: invalid memory capacity '%s'; using stock memory\n",
val);
}
static
bool
opera_lr_opts_set_mem_cfg(opera_lr_opts_t const *opts_)
{
if(g_OPTS.initialized_opera)
return true;
if(opera_mem_init(opts_->mem_cfg) != 0)
{
retro_log_printf_cb(RETRO_LOG_ERROR,"[Opera]: unable to initialize memory\n");
return false;
}
g_OPTS.mem_cfg = opts_->mem_cfg;
return true;
}
static
void
opera_lr_opts_get_video_buffer(opera_lr_opts_t *opts_)
{
(void)opts_;
}
static
void
opera_lr_opts_set_video_buffer(opera_lr_opts_t const *opts_)
{
uint32_t size;
if(g_OPTS.initialized_opera)
return;
if(g_OPTS.video_buffer)
return;
/*
* The 4x multiplication is for hires mode
* 4 bytes per pixel will handle any format
* Wastes some memory but simplifies things
*/
size = (opera_region_max_width() * opera_region_max_height() * 4);
g_OPTS.video_buffer = (uint32_t*)calloc(size,sizeof(uint32_t));
opera_vdlp_set_video_buffer(g_OPTS.video_buffer);
}
static
int64_t
read_file_from_system_directory(const char *filename_,
uint8_t *data_,
int64_t size_)
{
int64_t rv;
RFILE *file;
const char *system_path;
char fullpath[PATH_MAX_LENGTH];
system_path = NULL;
rv = retro_environment_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY,&system_path);
if((rv == 0) || (system_path == NULL))
return -1;
fill_pathname_join(fullpath,system_path,filename_,PATH_MAX_LENGTH);
file = filestream_open(fullpath,RETRO_VFS_FILE_ACCESS_READ,RETRO_VFS_FILE_ACCESS_HINT_NONE);
if(file == NULL)
return -1;
rv = filestream_read(file,data_,size_);
filestream_close(file);
return rv;
}
static
void
opera_lr_opts_get_bios(opera_lr_opts_t *opts_)
{
const char *val;
const opera_bios_t *bios;
opts_->bios = opera_bios_builtin();
val = getval("bios");
if(val == NULL)
return;
if(strcmp(val,OPERA_BIOS_BUILTIN_ID) == 0)
return;
for(bios = opera_bios_begin(); bios != opera_bios_end(); bios++)
{
if(bios == opera_bios_builtin())
continue;
if(strcmp(bios->filename,val))
continue;
opts_->bios = bios;
return;
}
}
static
int64_t
_load_bios_rom(const opera_bios_t *bios_)
{
int64_t rv;
assert(bios_ != NULL);
if(bios_ == opera_bios_builtin())
{
// Memory initialization provides fresh zeroed ROM1, including its unused tail.
rv = opera_bios_load_builtin(ROM1,ROM1_SIZE);
if(rv < 0)
retro_log_printf_cb(RETRO_LOG_ERROR,
"[Opera]: unable to decompress built-in BIOS ROM - %s\n",
bios_->name);
else
retro_log_printf_cb(RETRO_LOG_INFO,
"[Opera]: loaded built-in BIOS ROM - %s\n",
bios_->name);
return rv;
}
rv = read_file_from_system_directory(bios_->filename,ROM1,ROM1_SIZE);
if(rv < 0)
retro_log_printf_cb(RETRO_LOG_ERROR,
"[Opera]: unable to find or load BIOS ROM - %s\n",
bios_->filename);
else
retro_log_printf_cb(RETRO_LOG_INFO,
"[Opera]: loaded BIOS ROM - %s\n",
bios_->filename);
return rv;
}
static
void
opera_lr_opts_set_bios(opera_lr_opts_t const *opts_)
{
const opera_bios_t *bios;
if(g_OPTS.initialized_opera)
return;
bios = (opts_->bios == NULL) ? opera_bios_builtin() : opts_->bios;
if(_load_bios_rom(bios) < 0)
return;
opera_mem_rom1_byteswap32_if_le();
g_OPTS.bios = bios;
}
static
void
opera_lr_opts_get_font(opera_lr_opts_t *opts_)
{
const char *val;
const opera_bios_t *font;
opts_->font = NULL;
val = getval("font");
if(val == NULL)
return;
for(font = opera_bios_font_begin(); font != opera_bios_font_end(); font++)
{
if(strcmp(font->filename,val))
continue;
opts_->font = font;
return;
}
}
static
void
opera_lr_opts_set_font(opera_lr_opts_t const *opts_)
{
int64_t rv;
if(g_OPTS.initialized_opera)
return;
if(opts_->font == NULL)
{
opera_mem_rom2_clear();
return;
}
rv = read_file_from_system_directory(opts_->font->filename,ROM2,ROM2_SIZE);
if(rv < 0)
{
retro_log_printf_cb(RETRO_LOG_ERROR,
"[Opera]: unable to find or load FONT ROM - %s\n",
opts_->font->filename);
return;
}
retro_log_printf_cb(RETRO_LOG_INFO,
"[Opera]: loaded FONT ROM - %s\n",
opts_->font->filename);
opera_mem_rom2_byteswap32_if_le();
g_OPTS.font = opts_->font;
}
static
void
opera_lr_opts_get_nvram_shared(opera_lr_opts_t *opts_)
{
opts_->nvram_shared = getval_is_str("nvram_storage",
"shared",
true);
}
static
void
opera_lr_opts_set_nvram_shared(opera_lr_opts_t const *opts_)
{
g_OPTS.nvram_shared = opts_->nvram_shared;
}
static
void
opera_lr_opts_get_nvram_version(opera_lr_opts_t *opts_)
{
opts_->nvram_version = getval_as_int("nvram_version",0);
}
static
void
opera_lr_opts_set_nvram_version(opera_lr_opts_t const *opts_)
{
g_OPTS.nvram_version = opts_->nvram_version;
}
static
void
opera_lr_opts_get_region(opera_lr_opts_t *opts_)
{
const char *val;
opts_->region = OPERA_REGION_NTSC;
val = getval("region");
if(val == NULL)
return;
if(!strcmp(val,"ntsc"))
opts_->region = OPERA_REGION_NTSC;
else if(!strcmp(val,"pal1"))
opts_->region = OPERA_REGION_PAL1;
else if(!strcmp(val,"pal2"))
opts_->region = OPERA_REGION_PAL2;
}
static
void
opera_lr_opts_set_region(opera_lr_opts_t const *opts_)
{
switch(opts_->region)
{
default:
case OPERA_REGION_NTSC:
opera_region_set_NTSC();
break;
case OPERA_REGION_PAL1:
opera_region_set_PAL1();
break;
case OPERA_REGION_PAL2:
opera_region_set_PAL2();
break;
}
g_OPTS.region = opts_->region;
g_OPTS.video_width = (opera_region_width() << g_OPTS.high_resolution);
g_OPTS.video_height = (opera_region_height() << g_OPTS.high_resolution);
}
static
void
opera_lr_opts_get_cd_speed(opera_lr_opts_t *opts_)
{
const char *val = getval("cd_speed");
if(val == NULL)
{
opts_->cd_speed = 2;
return;
}
if(!strcmp(val,"unbounded"))
{
opts_->cd_speed = 0;
return;
}
opts_->cd_speed = atoi(val);
switch(opts_->cd_speed)
{
case 1:
case 2:
case 4:
case 8:
case 16:
case 24:
case 32:
break;
default:
opts_->cd_speed = 0;
break;
}
}
static
void
opera_lr_opts_set_cd_speed(opera_lr_opts_t const *opts_)
{
opera_cdrom_set_speed(opts_->cd_speed);
g_OPTS.cd_speed = opts_->cd_speed;
}
static
void
opera_lr_opts_get_cpu_overclock(opera_lr_opts_t *opts_)
{
opts_->cpu_overclock = getval_as_float("cpu_overclock",1.0);
}
static
void
opera_lr_opts_set_cpu_overclock(opera_lr_opts_t const *opts_)
{
opera_clock_cpu_set_freq_mul(opts_->cpu_overclock);
g_OPTS.cpu_overclock = opts_->cpu_overclock;
}
static
void
opera_lr_opts_get_vdlp_pixel_format(opera_lr_opts_t *opts_)
{
const char *val;
opts_->vdlp_pixel_format = VDLP_PIXEL_FORMAT_RGB565;
opts_->video_pitch_shift = 1;
val = getval("vdlp_pixel_format");
if(val == NULL)
return;
if(!strcmp(val,"XRGB8888"))
{
opts_->vdlp_pixel_format = VDLP_PIXEL_FORMAT_XRGB8888;
opts_->video_pitch_shift = 2;
}
else if(!strcmp(val,"RGB565"))
{
opts_->vdlp_pixel_format = VDLP_PIXEL_FORMAT_RGB565;
opts_->video_pitch_shift = 1;
}
else if(!strcmp(val,"0RGB1555"))
{
opts_->vdlp_pixel_format = VDLP_PIXEL_FORMAT_0RGB1555;
opts_->video_pitch_shift = 1;
}
}
static
void
opera_lr_opts_set_vdlp_pixel_format(opera_lr_opts_t const *opts_)
{
uint32_t flags;
if(g_OPTS.initialized_libretro)
return;
opera_vdlp_set_pixel_format(opts_->vdlp_pixel_format);
g_OPTS.vdlp_pixel_format = opts_->vdlp_pixel_format;
g_OPTS.video_pitch_shift = opts_->video_pitch_shift;
}
static
void
opera_lr_opts_get_vdlp_bypass_clut(opera_lr_opts_t *opts_)
{
opts_->vdlp_bypass_clut = getval_is_enabled("vdlp_bypass_clut",false);
}
static
void
opera_lr_opts_set_vdlp_bypass_clut(opera_lr_opts_t const *opts_)
{
int rv;
rv = opera_vdlp_set_bypass_clut(opts_->vdlp_bypass_clut);
if(rv != 0)
return;
g_OPTS.vdlp_bypass_clut = opts_->vdlp_bypass_clut;
}
static
void
opera_lr_opts_get_high_resolution(opera_lr_opts_t *opts_)
{
opts_->high_resolution = getval_is_enabled("high_resolution",false);
}
static
void
opera_lr_opts_set_high_resolution(opera_lr_opts_t const *opts_)
{
opera_vdlp_set_hires(opts_->high_resolution);
g_OPTS.high_resolution = opts_->high_resolution;
HIRESMODE = g_OPTS.high_resolution;
g_OPTS.video_width = (opera_region_width() << g_OPTS.high_resolution);
g_OPTS.video_height = (opera_region_height() << g_OPTS.high_resolution);
}
static
void
opera_lr_opts_get_active_devices(opera_lr_opts_t *opts_)
{
unsigned rv;
opts_->active_devices = getval_as_int("active_devices",1);
if(opts_->active_devices > LR_INPUT_MAX_DEVICES)
opts_->active_devices = 1;
}
static
void
opera_lr_opts_set_active_devices(opera_lr_opts_t const *opts_)
{
g_OPTS.active_devices = opts_->active_devices;
}
static
void
opera_lr_opts_get_hide_lightgun_crosshairs(opera_lr_opts_t *opts_)
{
opts_->hide_lightgun_crosshairs = getval_is_enabled("hide_lightgun_crosshairs",false);
}
static
void
opera_lr_opts_set_hide_lightgun_crosshairs(opera_lr_opts_t const *opts_)
{
g_OPTS.hide_lightgun_crosshairs = opts_->hide_lightgun_crosshairs;
}
static
void
opera_lr_opts_get_madam_matrix_engine(opera_lr_opts_t *opts_)
{
const char *val;
opts_->madam_matrix_engine = "hardware";
val = getval("madam_matrix_engine");
if(val == NULL)
return;
if(!strcmp(val,"software"))
opts_->madam_matrix_engine = "software";
else
opts_->madam_matrix_engine = "hardware";
}
static
void
opera_lr_opts_set_madam_matrix_engine(opera_lr_opts_t const *opts_)
{
if(g_OPTS.initialized_opera)
return;
if(!strcmp(opts_->madam_matrix_engine,"software"))
opera_madam_me_mode_software();
else
opera_madam_me_mode_hardware();
g_OPTS.madam_matrix_engine = opts_->madam_matrix_engine;
}
static
void
opera_lr_opts_get_kprint(opera_lr_opts_t *opts_)
{
opts_->kprint = getval_is_enabled("kprint",false);
}
static
void
opera_lr_opts_set_kprint(opera_lr_opts_t const *opts_)
{
if(opts_->kprint)
opera_madam_kprint_enable();
else
opera_madam_kprint_disable();
g_OPTS.kprint = opts_->kprint;
}
static
void
opera_lr_opts_get_dsp_threaded(opera_lr_opts_t *opts_)
{
opts_->dsp_threaded = getval_is_enabled("dsp_threaded",false);
}
static
void
opera_lr_opts_set_dsp_threaded(opera_lr_opts_t const *opts_)
{
opera_lr_dsp_init(opts_->dsp_threaded);
g_OPTS.dsp_threaded = opts_->dsp_threaded;
}
static
void
opera_lr_opts_get_swi_hle(opera_lr_opts_t *opts_)
{
opts_->swi_hle = getval_is_enabled("swi_hle",false);
}
static
void
opera_lr_opts_set_swi_hle(opera_lr_opts_t const *opts_)
{
opera_arm_swi_hle_set(opts_->swi_hle);
g_OPTS.swi_hle = opts_->swi_hle;
}
void
opera_lr_opts_get(opera_lr_opts_t *opts_)
{
opera_lr_opts_get_mem_cfg(opts_);
opera_lr_opts_get_video_buffer(opts_);
opera_lr_opts_get_vdlp_pixel_format(opts_);
opera_lr_opts_get_bios(opts_);
opera_lr_opts_get_font(opts_);
opera_lr_opts_get_madam_matrix_engine(opts_);
opera_lr_opts_get_active_devices(opts_);
opera_lr_opts_get_cd_speed(opts_);
opera_lr_opts_get_cpu_overclock(opts_);
opera_lr_opts_get_dsp_threaded(opts_);
opera_lr_opts_get_hide_lightgun_crosshairs(opts_);
opera_lr_opts_get_high_resolution(opts_);
opera_lr_opts_get_kprint(opts_);
opera_lr_opts_get_nvram_shared(opts_);
opera_lr_opts_get_nvram_version(opts_);
opera_lr_opts_get_region(opts_);
opera_lr_opts_get_swi_hle(opts_);
opera_lr_opts_get_vdlp_bypass_clut(opts_);
}
static
bool
opera_lr_opts_set(opera_lr_opts_t const *opts_)
{
// Can only be set at start/restart
if(!opera_lr_opts_set_mem_cfg(opts_))
return false;
opera_lr_opts_set_video_buffer(opts_);
opera_lr_opts_set_vdlp_pixel_format(opts_);
opera_lr_opts_set_bios(opts_);
opera_lr_opts_set_font(opts_);
opera_lr_opts_set_madam_matrix_engine(opts_);
// Can be updated at any time
opera_lr_opts_set_active_devices(opts_);
opera_lr_opts_set_cd_speed(opts_);
opera_lr_opts_set_cpu_overclock(opts_);
opera_lr_opts_set_dsp_threaded(opts_);
opera_lr_opts_set_hide_lightgun_crosshairs(opts_);
opera_lr_opts_set_high_resolution(opts_);
opera_lr_opts_set_kprint(opts_);
opera_lr_opts_set_nvram_shared(opts_);
opera_lr_opts_set_nvram_version(opts_);
opera_lr_opts_set_region(opts_);
opera_lr_opts_set_swi_hle(opts_);
opera_lr_opts_set_vdlp_bypass_clut(opts_);
g_OPTS.initialized_libretro = true;
g_OPTS.initialized_opera = true;
return true;
}
static
uint32_t
opera_lr_opts_changes(opera_lr_opts_t const *opts_)
{
uint32_t changes;
changes = OPERA_LR_OPTS_CHANGE_NONE;
if(!opts_->initialized_libretro)
return changes;
if(opts_->region != g_OPTS.region)
changes |= (OPERA_LR_OPTS_CHANGE_REGION |
OPERA_LR_OPTS_CHANGE_TIMING |
OPERA_LR_OPTS_CHANGE_GEOMETRY);
if(opts_->high_resolution != g_OPTS.high_resolution)
changes |= OPERA_LR_OPTS_CHANGE_GEOMETRY;
return changes;
}
uint32_t
opera_lr_opts_process()
{
uint32_t changes;
opera_lr_opts_t old_opts;
opera_lr_opts_t opts = {0};
old_opts = g_OPTS;
opera_lr_opts_get(&opts);
if(!opera_lr_opts_set(&opts))
return OPERA_LR_OPTS_CHANGE_ERROR;
changes = opera_lr_opts_changes(&old_opts);
return changes;
}
void
opera_lr_opts_reset()
{
opera_lr_opts_t opts;
opts = g_OPTS;
if(g_OPTS.video_buffer)
free(g_OPTS.video_buffer);
opera_lr_dsp_destroy();
opera_mem_destroy();
memset(&g_OPTS,0,sizeof(g_OPTS));
g_OPTS.initialized_libretro = opts.initialized_libretro;
g_OPTS.vdlp_pixel_format = opts.vdlp_pixel_format;
g_OPTS.video_pitch_shift = opts.video_pitch_shift;
}
void
opera_lr_opts_destroy()
{
opera_lr_opts_reset();
}