Files
picogus/sw/cdrom/cdrom.c
T

681 lines
23 KiB
C

/*
* 86Box A hypervisor and IBM PC system emulator that specializes in
* running old operating systems and software designed for IBM
* PC systems and compatibles from 1981 through fairly recent
* system designs based on the PCI bus.
*
* This file is part of the 86Box distribution.
*
* Generic CD-ROM drive core.
*
*
*
* Authors: Miran Grca, <mgrca8@gmail.com>,
* Kevin Moonlight, <me@yyzkevin.com>
* Ian Scott, <ian@polpo.org>
*
* Copyright 2018-2021 Miran Grca.
* Copyright (C) 2024 Kevin Moonlight
* Copyright (C) 2025 Ian Scott
*/
#include <inttypes.h>
#include <stdarg.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
#define HAVE_STDARG_H
#include "../include/pg_debug.h"
#include "86box_compat.h"
#include "cdrom.h"
#include "cdrom_image_manager.h"
#include "cdrom_error_msg.h"
#include "pico/multicore.h"
#include "hardware/structs/timer.h"
#include "audio/volctrl.h"
int32_t cd_audio_volume = (1 << VOLCTRL_FRACT_BITS);
/* The addresses sent from the guest are absolute, ie. a LBA of 0 corresponds to a MSF of 00:00:00. Otherwise, the counter displayed by the guest is wrong:
there is a seeming 2 seconds in which audio plays but counter does not move, while a data track before audio jumps to 2 seconds before the actual start
of the audio while audio still plays. With an absolute conversion, the counter is fine. */
#undef MSFtoLBA
#define MSFtoLBA(m, s, f) ((((m * 60) + s) * 75) + f)
#pragma pack(push, 1)
typedef struct {
uint8_t user_data[2048],
ecc[288];
} m1_data_t;
typedef struct {
uint8_t sub_header[8],
user_data[2328];
} m2_data_t;
typedef union {
m1_data_t m1_data;
m2_data_t m2_data;
uint8_t raw_data[2336];
} sector_data_t;
typedef struct {
uint8_t sync[12];
uint8_t header[4];
sector_data_t data;
} sector_raw_data_t;
typedef union {
sector_raw_data_t sector_data;
uint8_t raw_data[2352];
} sector_t;
typedef struct {
sector_t sector;
uint8_t c2[296];
uint8_t subchannel_raw[96];
uint8_t subchannel_q[16];
uint8_t subchannel_rw[96];
} cdrom_sector_t;
typedef union {
cdrom_sector_t cdrom_sector;
uint8_t buffer[2856];
} sector_buffer_t;
#pragma pack(pop)
static int cdrom_sector_size;
static uint8_t raw_buffer[2856]; /* Needs to be the same size as sector_buffer_t in the structs. */
static uint8_t extra_buffer[296];
/* int cdrom_interface_current; */
#define PGDEBUG_CDROM 0
#define cdrom_log(fmt, ...) do { if (PGDEBUG_CDROM) DBG_PRINTF(fmt "\n", ##__VA_ARGS__); } while(0)
/*
static __inline int
bin2bcd(int x)
{
return (x % 10) | ((x / 10) << 4);
}
*/
/* unused
// Avoid div and modulo by using magic multiplication, this is about 10 cycles
static __inline uint8_t
bin2bcd(uint8_t x)
{
// Calculate tens digit: value / 10 = (value * 205) >> 11
uint8_t tens = (uint8_t)(((uint32_t)x * 205) >> 11);
return (tens << 4) | (x - (tens * 10));
}
static __inline uint8_t
bcd2bin(uint8_t x)
{
return (x >> 4) * 10 + (x & 0x0f);
}
*/
void cdrom_tasks(cdrom_t *dev) {
// Will almost always be CD_COMMAND_NONE, so use __builtin_expect to tell the compiler
switch (__builtin_expect(dev->image_command, CD_COMMAND_NONE)) {
case CD_COMMAND_NONE:
cdrom_read_data(dev);
break;
case CD_COMMAND_IMAGE_LIST:
cdrom_errorstr_clear();
dev->image_list = cdman_list_images(&dev->image_count);
dev->image_command = CD_COMMAND_NONE;
dev->image_status = dev->image_list ? CD_STATUS_READY : CD_STATUS_ERROR;
break;
case CD_COMMAND_IMAGE_LOAD_INDEX:
cdman_load_image_index(dev, dev->image_data);
break;
case CD_COMMAND_IMAGE_LOAD:
DBG_PRINTF("loading");
cdrom_errorstr_clear();
if (dev->disk_loaded) {
dev->disk_loaded = 0;
dev->media_changed = 1;
cdrom_image_close(dev);
DBG_PRINTF("Disk Removed.\n");
cdrom_error(dev, 0x11); //media changed
}
if (dev->image_path[0]) {
DBG_PRINTF("Opening %s...",dev->image_path);
if (cdrom_image_open(dev,dev->image_path)) {
dev->image_command = CD_COMMAND_NONE;
dev->image_status = CD_STATUS_ERROR;
cdman_clear_image();
break;
} else {
cdman_set_image_index(dev);
}
DBG_PRINTF("Done.\n");
dev->disk_loaded = 1;
dev->media_changed = 1;
cdrom_error(dev, 0x11); //media changed
}
dev->image_command = CD_COMMAND_NONE;
dev->image_status = CD_STATUS_IDLE;
break;
}
}
void cdrom_fifo_init(cdrom_fifo_t *fifo,uint16_t size) {
fifo->data = calloc(1, size);
if(!fifo->data) fatal("Unable to allocate fifo");
fifo->size = size;
}
void cdrom_fifo_write(cdrom_fifo_t *fifo,uint8_t val) {
/* if(!fifo) return; */
fifo->data[fifo->tail]=val;
fifo->tail++;
if(fifo->tail >= fifo->size) fifo->tail=0;
}
void cdrom_fifo_write_multiple(cdrom_fifo_t *fifo, const uint8_t *data, size_t data_size) {
// Calculate the number of bytes until the end of the buffer
size_t bytes_until_end = fifo->size - fifo->tail;
if (bytes_until_end >= data_size) {
// Simple case - no wrap-around needed
memcpy(fifo->data + fifo->tail, data, data_size);
if (bytes_until_end == data_size) {
fifo->tail = 0;
} else {
fifo->tail += data_size;
}
} else {
// Handle wrap-around: first copy until the end of the buffer
memcpy(fifo->data + fifo->tail, data, bytes_until_end);
// Then copy the remaining data to the beginning of the buffer
memcpy(fifo->data, data + bytes_until_end, data_size - bytes_until_end);
fifo->tail = data_size - bytes_until_end;
}
}
uint16_t __inline cdrom_fifo_level(cdrom_fifo_t *fifo) {
/* if(!fifo) return 0; */
if(fifo->head <= fifo->tail) return fifo->tail - fifo->head;
return fifo->tail - fifo->head + fifo->size;
}
uint8_t cdrom_fifo_read(cdrom_fifo_t *fifo) {
uint8_t x;
if(!cdrom_fifo_level(fifo)) {
DBG_PRINTF("FIFO UNDERRUN(%p)\n",fifo);
return 0;
}
x = fifo->data[fifo->head];
fifo->head++;
if(fifo->head >= fifo->size) fifo->head=0;
return x;
}
void cdrom_fifo_clear(cdrom_fifo_t *fifo) {
if(cdrom_fifo_level(fifo)) {
DBG_PRINTF("*** DISCARD %u *** (%p)\n",cdrom_fifo_level(fifo),fifo);
fifo->tail = fifo->head;
}
}
void
cdrom_stop(cdrom_t *dev)
{
if (dev->cd_status > CD_STATUS_DATA_ONLY)
dev->cd_status = CD_STATUS_STOPPED;
dev->audio_staging_sectors = 0;
dev->audio_staging_index = 0;
dev->audio_sector_consumed_pairs = 0;
#if USE_CD_AUDIO_FIFO
fifo_reset(&dev->audio_fifo);
#endif
}
uint8_t cdrom_seek(cdrom_t *dev, int m, int s, int f) {
uint32_t pos;
if (!dev) return 0;
cdrom_log("CD-ROM %i: Seek to M:%x S:%x F:%x\n", dev->id, m,s,f);
pos = MSFtoLBA(m, s, f) - 150;
//TODO: Should i check if this is a valid seek?
dev->seek_pos = pos;
cdrom_stop(dev);
return 1;
}
#if USE_CD_AUDIO_FIFO
audio_fifo_t* cdrom_audio_fifo_peek(cdrom_t *dev) {
return &dev->audio_fifo;
}
void cdrom_audio_fifo_init(cdrom_t *dev) {
fifo_init(&dev->audio_fifo);
}
bool cdrom_audio_callback(cdrom_t *dev, uint32_t len) {
bool ret = true;
if (dev->cd_status != CD_STATUS_PLAYING) {
return false;
}
// --- Fill audio_fifo from staging buffer, batch-reading new sectors as needed ---
while (fifo_level(&dev->audio_fifo) < len && ret) {
// Advance to next sector in batch if current one is fully consumed
if (dev->audio_sector_consumed_pairs >= STEREO_SAMPLES_PER_SECTOR) {
dev->audio_staging_index++;
dev->audio_sector_consumed_pairs = 0;
}
// Read a new batch when the staging buffer is exhausted
if (dev->audio_staging_index >= dev->audio_staging_sectors) {
if (dev->seek_pos >= dev->cd_end) {
cdrom_log("CD-ROM %i: Playing completed (reached cd_end)\n", dev->id);
dev->cd_status = CD_STATUS_PLAYING_COMPLETED;
ret = false;
break;
}
uint32_t avail = dev->cd_end - dev->seek_pos;
uint32_t batch = (avail < AUDIO_SECTOR_BATCH) ? avail : AUDIO_SECTOR_BATCH;
int sectors_read;
if (dev->audio_muted_soft) {
cdrom_log("CD-ROM %i: Muted. Faking batch read of %u sectors with silence.\n", dev->id, batch);
memset(dev->audio_sector_buffer, 0, batch * RAW_SECTOR_SIZE);
sectors_read = batch;
} else {
sectors_read = dev->ops->read_audio_sectors(dev, dev->audio_sector_buffer, dev->seek_pos, batch);
}
if (sectors_read > 0) {
cdrom_log("CD-ROM %i: Batch read %u sectors at LBA %08X\n", dev->id, sectors_read, dev->seek_pos);
dev->seek_pos += sectors_read;
dev->audio_staging_sectors = sectors_read;
dev->audio_staging_index = 0;
dev->audio_sector_consumed_pairs = 0;
} else {
cdrom_log("CD-ROM %i: Batch read at LBA %08X failed\n", dev->id, dev->seek_pos);
dev->cd_status = CD_STATUS_STOPPED;
ret = false;
break;
}
}
// Point into the current sector within the staging buffer
dev->current_sector_samples = (sample_pair *)(dev->audio_sector_buffer
+ dev->audio_staging_index * RAW_SECTOR_SIZE);
uint32_t pairs_remaining = STEREO_SAMPLES_PER_SECTOR - dev->audio_sector_consumed_pairs;
uint32_t space_in_fifo = fifo_free_space(&dev->audio_fifo);
uint32_t pairs_to_transfer = (pairs_remaining < space_in_fifo) ?
pairs_remaining : space_in_fifo;
bool added_ok = fifo_add_samples(&dev->audio_fifo,
&dev->current_sector_samples[dev->audio_sector_consumed_pairs],
pairs_to_transfer);
if (added_ok) {
dev->audio_sector_consumed_pairs += pairs_to_transfer;
} else {
DBG_PUTCHAR('n');
cdrom_log("CD-ROM %i: fifo_add_samples failed unexpectedly! Space was %u, tried to add %u.\n",
dev->id, space_in_fifo, pairs_to_transfer);
ret = false;
break;
}
}
cdrom_log("CD-ROM %i: Audio cb. FIFO level: %u. Staging consumed: %d/%d. Ret %d\n",
dev->id, fifo_level(&dev->audio_fifo),
dev->audio_staging_index, dev->audio_staging_sectors, ret);
return ret;
}
#endif // USE_CD_AUDIO_FIFO
uint32_t cdrom_audio_callback_simple(cdrom_t *dev, int16_t *buffer, uint32_t len, bool pad) {
if (dev->cd_status != CD_STATUS_PLAYING) {
return 0;
}
uint32_t pairs_produced = 0;
uint32_t pairs_requested = len / 2;
// --- Fill buffer from staging buffer, batch-reading new sectors as needed ---
while (pairs_produced < pairs_requested) {
// Advance to next sector in batch if current one is fully consumed
if (dev->audio_sector_consumed_pairs >= STEREO_SAMPLES_PER_SECTOR) {
dev->audio_staging_index++;
dev->audio_sector_consumed_pairs = 0;
}
// Read a new batch when the staging buffer is exhausted
if (dev->audio_staging_index >= dev->audio_staging_sectors) {
if (dev->seek_pos >= dev->cd_end) {
cdrom_log("CD-ROM %i: Playing completed (reached cd_end)\n", dev->id);
dev->cd_status = CD_STATUS_PLAYING_COMPLETED;
break;
}
uint32_t avail = dev->cd_end - dev->seek_pos;
uint32_t batch = (avail < AUDIO_SECTOR_BATCH) ? avail : AUDIO_SECTOR_BATCH;
int sectors_read;
if (dev->audio_muted_soft) {
cdrom_log("CD-ROM %i: Muted. Faking batch read of %u sectors with silence.\n", dev->id, batch);
memset(dev->audio_sector_buffer, 0, batch * RAW_SECTOR_SIZE);
sectors_read = batch;
} else {
sectors_read = dev->ops->read_audio_sectors(dev, dev->audio_sector_buffer, dev->seek_pos, batch);
}
if (sectors_read > 0) {
cdrom_log("CD-ROM %i: Batch read %u sectors at LBA %08X\n", dev->id, sectors_read, dev->seek_pos);
dev->seek_pos += sectors_read;
dev->audio_staging_sectors = sectors_read;
dev->audio_staging_index = 0;
dev->audio_sector_consumed_pairs = 0;
} else {
cdrom_log("CD-ROM %i: Batch read at LBA %08X failed\n", dev->id, dev->seek_pos);
dev->cd_status = CD_STATUS_STOPPED;
break;
}
}
// Point into the current sector within the staging buffer
dev->current_sector_samples = (sample_pair *)(dev->audio_sector_buffer
+ dev->audio_staging_index * RAW_SECTOR_SIZE);
uint32_t pairs_remaining = STEREO_SAMPLES_PER_SECTOR - dev->audio_sector_consumed_pairs;
uint32_t space_in_buffer = pairs_requested - pairs_produced;
uint32_t pairs_to_transfer = (pairs_remaining < space_in_buffer) ?
pairs_remaining : space_in_buffer;
for (uint32_t i = 0; i < pairs_to_transfer; i++)
{
sample_pair sp = dev->current_sector_samples[dev->audio_sector_consumed_pairs + i];
buffer[(pairs_produced + i) * 2] = (int16_t)scale_sample(sp.data16[0], cd_audio_volume, 0);
buffer[(pairs_produced + i) * 2 + 1] = (int16_t)scale_sample(sp.data16[1], cd_audio_volume, 0);
}
pairs_produced += pairs_to_transfer;
dev->audio_sector_consumed_pairs += pairs_to_transfer;
}
uint32_t samples_produced = pairs_produced * 2;
if (!pad) {
return samples_produced;
} else {
// If we provided fewer samples than requested, fill the remainder of buffer with silence.
if (samples_produced < len) {
cdrom_log("CD-ROM %i: Outputting %u samples, padding %u with silence.\n",
dev->id, samples_produced, (len - samples_produced));
memset(buffer + samples_produced, 0, (size_t)(len - samples_produced) * sizeof(int16_t));
}
return len;
}
}
void __inline cdrom_read_data(cdrom_t *dev) {
uint32_t pos;
/* uint16_t x; */
/* uint8_t m,s,f; */
/*
TODO, need to handle when multiple blocks are requested.
*/
if(dev->req_total) {
/* while(dev->req_total) { */
if(cdrom_fifo_level(&dev->data_fifo) >= 2048) return;//need to be empty.
pos = MSFtoLBA(dev->req_m,dev->req_s,dev->req_f) - 150;
pos += dev->req_cur;
// Read CD sector directly from fatfs into data fifo - note this assumes
// 2048 byte sectors
dev->ops->read_sector(dev, CD_READ_DATA, dev->data_fifo.data + dev->data_fifo.tail, pos);
dev->data_fifo.tail = (dev->data_fifo.tail + 2048) & 4095;
dev->req_cur++;
if(dev->req_cur == dev->req_total) {
cdrom_output_status(dev);
dev->req_total=0;
}
}
}
uint8_t cdrom_status(cdrom_t *dev) {
uint8_t status = 0; /* initialise to avoid returning random stack garbage */
status |= 2; /* this bit seems to always be set? */
//bit 4 never set?
if(dev->cd_status == CD_STATUS_PLAYING) status |= STAT_PLAY;
if(dev->cd_status == CD_STATUS_PAUSED) status |= STAT_PLAY;
if(cdrom_has_errors(dev)) status |= 0x10;
status |= 0x20;//always set?
status |= STAT_TRAY;
if(dev->disk_loaded) {
status |= STAT_DISK;
status |= STAT_READY;
}
return status;
}
void cdrom_output_status(cdrom_t *dev) {
cdrom_fifo_write(&dev->info_fifo,cdrom_status(dev));
dev->media_changed=0;
}
uint8_t cdrom_audio_playmsf(cdrom_t *dev, int m,int s, int f, int M, int S, int F) {
uint32_t pos;
uint32_t pos2;
//TODO
/*
This debug sequence is required to prevent crashing during quick seeking.
Unable to find what is happening, but it is related to core1 cdrom audio callback interacting with the
buffer, while a command comes that also changes it, goes nuts ends up corrupting memory.
*/
/*
printf("Buf1: %i\n",dev->cd_buflen);
busy_wait_us(5);
printf("Buf2: %i\n",dev->cd_buflen);
*/
if (dev->cd_status == CD_STATUS_DATA_ONLY) {
return 0;
}
dev->audio_muted_soft = 0;
/* Do this at this point, since it's at this point that we know the
actual LBA position to start playing from. */
pos = MSFtoLBA(m,s,f) - 150;
pos2 = MSFtoLBA(M,S,F) - 150;
if (!(dev->ops->track_type(dev, pos) & CD_TRACK_AUDIO)) {
DBG_PRINTF("CD-ROM %i: LBA %08X not on an audio track\n", dev->id, pos);
cdrom_log("CD-ROM %i: LBA %08X not on an audio track\n", dev->id, pos);
cdrom_error(dev,0x0E);
cdrom_error(dev,0x10);
cdrom_stop(dev);
return 0;
}
/*
Changing these seek positions during audio playback is a problemo.
*/
dev->seek_pos = pos;
dev->cd_end = pos2;
dev->audio_staging_sectors = 0;
dev->audio_staging_index = 0;
dev->audio_sector_consumed_pairs = 0;
#if USE_CD_AUDIO_FIFO
fifo_reset(&dev->audio_fifo);
#endif
/* Memory barrier: ensure seek_pos, cd_end, and fifo_reset are all
* visible to core 1 before cd_status is set to PLAYING. Without this,
* the Cortex-M0+ compiler or hardware could reorder the store to
* cd_status ahead of the others, causing core 1 to start consuming
* from a partially-updated state. */
__dmb();
dev->cd_status = CD_STATUS_PLAYING;
return 1;
}
void
cdrom_audio_pause_resume(cdrom_t *dev, uint8_t resume)
{
if ((dev->cd_status == CD_STATUS_PLAYING) || (dev->cd_status == CD_STATUS_PAUSED)) {
dev->cd_status = (dev->cd_status & 0xfe) | (resume & 0x01);
#if USE_CD_AUDIO_FIFO
/* Only flush the FIFO on pause/stop — not on resume.
* Flushing on resume discards already-decoded audio and forces a
* re-fill from scratch, causing a brief gap at the start of resumed
* playback. */
if (!resume)
fifo_reset(&dev->audio_fifo);
#endif
}
}
uint8_t cdrom_get_subq(cdrom_t *dev,uint8_t *b) {
subchannel_t subc;
/* seek_pos points to the next sector to be READ, not the one currently
* audible. Subtract the number of sectors already buffered in the FIFO
* so the reported position matches what the listener actually hears.
* Each sector holds STEREO_SAMPLES_PER_SECTOR stereo pairs.
* Guard against underflow when the FIFO is empty or seek_pos is at the
* start of the track. */
#if USE_CD_AUDIO_FIFO
{
uint32_t buffered_sectors = fifo_level(&dev->audio_fifo) / STEREO_SAMPLES_PER_SECTOR;
uint32_t report_pos = (buffered_sectors < dev->seek_pos) ?
(dev->seek_pos - buffered_sectors) : 0;
dev->ops->get_subchannel(dev, report_pos, &subc);
}
#else
dev->ops->get_subchannel(dev, dev->seek_pos, &subc);
#endif
b[0] = 0x80; //?
b[1] = subc.attr;
b[2] = subc.track;
b[3] = subc.index;
b[4] = subc.abs_m;
b[5] = subc.abs_s;
b[6] = subc.abs_f;
b[7] = subc.rel_m;
b[8] = subc.rel_s;
b[9] = subc.rel_f;
b[10]=0; //??
}
uint8_t cdrom_read_toc(cdrom_t *dev, unsigned char *b, uint8_t track) {
track_info_t ti;
int first_track;
int last_track;
dev->ops->get_tracks(dev, &first_track, &last_track);
// Handle lead-out track
if (track == 0xaa)
track = last_track + 1;
else if (track > last_track)
return 0;
dev->ops->get_track_info(dev, track, 0, &ti);
b[0]=0x0;
b[1]=ti.attr;
b[2]=ti.number;
b[3]=0;
b[4]=ti.m;
b[5]=ti.s;
b[6]=ti.f;
b[7]=0;
return 1;
}
uint8_t cdrom_disc_info(cdrom_t *dev, unsigned char *b) {
track_info_t ti;
int first_track;
int last_track;
dev->ops->get_tracks(dev, &first_track, &last_track);
dev->ops->get_track_info(dev, last_track+1, 0, &ti);
b[0]=0x0;
b[1]=first_track;
b[2]=last_track;
b[3]=ti.m;
b[4]=ti.s;
b[5]=ti.f;
return 1;
}
uint8_t cdrom_disc_capacity(cdrom_t *dev, unsigned char *b) {
track_info_t ti;
int first_track;
int last_track;
dev->ops->get_tracks(dev, &first_track, &last_track);
dev->ops->get_track_info(dev, last_track+1, 0, &ti);
b[0]=ti.m;
b[1]=ti.s;
b[2]= ti.f > 0 ? ti.f - 1 : 0; /* guard against frame 0 underflowing to 255 */
b[3]=0x08;
b[4]=0x00;
return 1;
}
/* Peform a master init on the entire module. */
void
cdrom_global_init(void)
{
/* Clear the global data. */
memset(&cdrom, 0x00, sizeof(cdrom));
cdrom.error_str = cdrom_errorstr_get();
cdrom.current_sector_samples = (sample_pair*)cdrom.audio_sector_buffer;
set_volume(CMD_CDVOL);
}
static void
cdrom_drive_reset(cdrom_t *dev)
{
dev->priv = NULL;
dev->insert = NULL;
dev->close = NULL;
dev->get_volume = NULL;
dev->get_channel = NULL;
}
void cdrom_error(cdrom_t *dev,uint8_t code) {
uint8_t x;
if(!code) return;
if(!dev) return;
for(x=0;x<8;x++) {
if(!dev->errors[x]) dev->errors[x]=code;
}
}
void cdrom_read_errors(cdrom_t *dev,uint8_t *b) {
uint8_t x;
if(!dev) return;
if(!b) return;
for(x=0;x<8;x++) {
b[x]=dev->errors[x];
dev->errors[x]=0;
}
}
uint8_t cdrom_has_errors(cdrom_t *dev) {
uint8_t x;
if(!dev) return 0;
for(x=0;x<8;x++) {
if(dev->errors[x]) return 1;
}
return 0;
}