apple2ix/src/video/video.c

617 lines
19 KiB
C

/*
* Apple // emulator for *ix
*
* This software package is subject to the GNU General Public License
* version 3 or later (your choice) as published by the Free Software
* Foundation.
*
* Copyright 2017 Aaron Culliney
*
*/
#include "common.h"
typedef struct backend_node_s {
struct backend_node_s *next;
long order;
video_backend_s *backend;
} backend_node_s;
static bool video_initialized = false;
static bool null_backend_running = true;
static backend_node_s *head = NULL;
static video_backend_s *currentBackend = NULL;
static pthread_t render_thread_id = 0;
static unsigned int cyclesFrameLast = 0;
static unsigned long dirty = 0UL;
static bool reset_scanner = false;
#if VIDEO_TRACING
static FILE *video_trace_fp = NULL;
static unsigned long frameCount = 0UL;
static unsigned long frameBegin = 0UL;
static unsigned long frameEnd = UINT_MAX;
#endif
// ----------------------------------------------------------------------------
// video scanner & generator
// scanline text page offsets
static uint16_t vert_offset_txt[TEXT_ROWS + /*VBL:*/ 8 + /*extra:*/1] = {
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, // 0-63 screen top
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, // 64-127 screen middle
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, // 128-191 screen bottom
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, // 192-255 VBL...
0x380, // 256,257,258,259,260,261
};
// scanline hires page offsets
static uint16_t vert_offset_hgr[SCANLINES_FRAME] = {
0x0000,0x0400,0x0800,0x0C00,0x1000,0x1400,0x1800,0x1C00,// 0-63 screen top
0x0080,0x0480,0x0880,0x0C80,0x1080,0x1480,0x1880,0x1C80,
0x0100,0x0500,0x0900,0x0D00,0x1100,0x1500,0x1900,0x1D00,
0x0180,0x0580,0x0980,0x0D80,0x1180,0x1580,0x1980,0x1D80,
0x0200,0x0600,0x0A00,0x0E00,0x1200,0x1600,0x1A00,0x1E00,
0x0280,0x0680,0x0A80,0x0E80,0x1280,0x1680,0x1A80,0x1E80,
0x0300,0x0700,0x0B00,0x0F00,0x1300,0x1700,0x1B00,0x1F00,
0x0380,0x0780,0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80,
0x0000,0x0400,0x0800,0x0C00,0x1000,0x1400,0x1800,0x1C00,// 64-127 screen middle
0x0080,0x0480,0x0880,0x0C80,0x1080,0x1480,0x1880,0x1C80,
0x0100,0x0500,0x0900,0x0D00,0x1100,0x1500,0x1900,0x1D00,
0x0180,0x0580,0x0980,0x0D80,0x1180,0x1580,0x1980,0x1D80,
0x0200,0x0600,0x0A00,0x0E00,0x1200,0x1600,0x1A00,0x1E00,
0x0280,0x0680,0x0A80,0x0E80,0x1280,0x1680,0x1A80,0x1E80,
0x0300,0x0700,0x0B00,0x0F00,0x1300,0x1700,0x1B00,0x1F00,
0x0380,0x0780,0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80,
0x0000,0x0400,0x0800,0x0C00,0x1000,0x1400,0x1800,0x1C00,// 128-191 screen bottom
0x0080,0x0480,0x0880,0x0C80,0x1080,0x1480,0x1880,0x1C80,
0x0100,0x0500,0x0900,0x0D00,0x1100,0x1500,0x1900,0x1D00,
0x0180,0x0580,0x0980,0x0D80,0x1180,0x1580,0x1980,0x1D80,
0x0200,0x0600,0x0A00,0x0E00,0x1200,0x1600,0x1A00,0x1E00,
0x0280,0x0680,0x0A80,0x0E80,0x1280,0x1680,0x1A80,0x1E80,
0x0300,0x0700,0x0B00,0x0F00,0x1300,0x1700,0x1B00,0x1F00,
0x0380,0x0780,0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80,
0x0000,0x0400,0x0800,0x0C00,0x1000,0x1400,0x1800,0x1C00,// 192-255 VBL...
0x0080,0x0480,0x0880,0x0C80,0x1080,0x1480,0x1880,0x1C80,
0x0100,0x0500,0x0900,0x0D00,0x1100,0x1500,0x1900,0x1D00,
0x0180,0x0580,0x0980,0x0D80,0x1180,0x1580,0x1980,0x1D80,
0x0200,0x0600,0x0A00,0x0E00,0x1200,0x1600,0x1A00,0x1E00,
0x0280,0x0680,0x0A80,0x0E80,0x1280,0x1680,0x1A80,0x1E80,
0x0300,0x0700,0x0B00,0x0F00,0x1300,0x1700,0x1B00,0x1F00,
0x0380,0x0780,0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80,
0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80, // 256,257,258,259,260,261
};
// scanline horizontal offsets (UtAIIe 5-12, 5-15+)
static uint8_t scan_offset[5][CYCLES_SCANLINE] =
{
{ // 0-63 screen top
0x68,
0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,// 1-8
0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,// 9-16
0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F,// 17-24
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,// 25-32
0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,// 33-40
0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,// 41-48
0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,// 49-56
0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,// 57-64
},
{ // 64-127 screen middle
0x10,
0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,
0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,
0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,
0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,
0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,
},
{ // 128-191 screen bottom
0x38,
0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,
0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,
0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,
0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,
0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,
0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,
},
{ // 192-255 VBL
0x60,
0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,
0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,
0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F,
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,
0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,
},
{ // 256,257,258,259,260,261
0x60,
0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,
0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,
0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F,
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,
0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,
},
};
// ----------------------------------------------------------------------------
void video_init(void) {
video_initialized = true;
ASSERT_NOT_ON_CPU_THREAD();
LOG("(re)setting render_thread_id : %lu -> %lu", (unsigned long)render_thread_id, (unsigned long)pthread_self());
render_thread_id = pthread_self();
currentBackend->init((void*)0);
}
void _video_setRenderThread(pthread_t id) {
LOG("setting render_thread_id : %lu -> %lu", (unsigned long)render_thread_id, (unsigned long)id);
render_thread_id = id;
}
bool video_isRenderThread(void) {
return (pthread_self() == render_thread_id);
}
void video_shutdown(void) {
#if MOBILE_DEVICE
// WARNING : shutdown should occur on the render thread. Platform code (iOS, Android) should ensure this is called
// from within a render pass...
assert(!render_thread_id || pthread_self() == render_thread_id);
#endif
currentBackend->shutdown();
}
void video_render(void) {
ASSERT_ON_UI_THREAD();
currentBackend->render();
}
void video_main_loop(void) {
currentBackend->main_loop();
}
void video_flashText(void) {
currentBackend->flashText();
}
bool video_isDirty(unsigned long flags) {
return !!(dirty & flags);
}
void video_setDirty(unsigned long flags) {
__sync_fetch_and_or(&dirty, flags);
if (flags & A2_DIRTY_FLAG) {
ASSERT_ON_CPU_THREAD();
video_scannerUpdate();
}
}
unsigned long video_clearDirty(unsigned long flags) {
return __sync_fetch_and_and(&dirty, ~flags);
}
// ----------------------------------------------------------------------------
// video scanner & generator routines
static inline uint16_t _getScannerAddress(drawpage_mode_t mode, int page, unsigned int vCount, unsigned int hCount) {
uint16_t a = 0;
if (mode == DRAWPAGE_TEXT) {
a = (page ? 0x800 : 0x400) + vert_offset_txt[vCount>>3] + scan_offset[vCount>>6][hCount];
} else {
a = (page ? 0x4000 : 0x2000) + vert_offset_hgr[vCount] + scan_offset[vCount>>6][hCount];
}
return a;
}
static drawpage_mode_t _currentMode(unsigned int vCount) {
// FIXME TODO ... this is currently incorrect in VBL for MIXED
drawpage_mode_t mode = (vCount < SCANLINES_MIX) ? video_currentMainMode(run_args.softswitches) : video_currentMixedMode(run_args.softswitches);
return mode;
}
static void _flushScanline(uint8_t *scanline, unsigned int scanrow, unsigned int scancol, unsigned int scanend) {
scan_data_t scandata = {
.scanline = &scanline[0],
.softswitches = run_args.softswitches,
.scanrow = scanrow,
.scancol = scancol,
.scanend = scanend,
};
currentBackend->flushScanline(&scandata);
}
void video_scannerReset(void) {
ASSERT_ON_CPU_THREAD();
reset_scanner = true;
}
// Call to advance the video scanner and generator when the following events occur:
// 1: Just prior writing to active video memory
// 2: Just prior toggling a video softswitch
// 3: Upon video frame completion (CYCLES_FRAME : 17030) -- FIXME TODO can we optimize this away if no change?
void video_scannerUpdate(void) {
static uint8_t scanline[CYCLES_VIS<<1] = { 0 }; // 80 columns of data ...
static unsigned int scancol = 0;
static unsigned int scanidx = 0;
static unsigned int hCount = 0;
static unsigned int vCount = 0;
ASSERT_ON_CPU_THREAD();
if (reset_scanner) {
reset_scanner = false;
cyclesFrameLast = 0;
scancol = 0;
scanidx = 0;
hCount = 0;
vCount = 0;
}
timing_checkpointCycles();
assert(cycles_video_frame >= cyclesFrameLast);
unsigned int cyclesCount = cycles_video_frame - cyclesFrameLast;
cyclesFrameLast = cycles_video_frame;
if (UNLIKELY(cyclesCount == 0)) {
return;
}
int page = video_currentPage(run_args.softswitches);
uint8_t aux = 0x0;
uint8_t mbd = 0x0;
uint16_t addr = 0x0;
drawpage_mode_t mode = _currentMode(vCount);
for (unsigned int i=0; i<cyclesCount; i++) {
const bool isVisible = ((hCount >= CYCLES_VIS_BEGIN) && (vCount < SCANLINES_VBL_BEGIN));
#if VIDEO_TRACING
char *type = "xBL";
#else
if (isVisible)
#endif
{
addr = _getScannerAddress(mode, page, vCount, hCount);
aux = apple_ii_64k[1][addr];
mbd = apple_ii_64k[0][addr];
}
if (isVisible) {
#if VIDEO_TRACING
type = "VIS";
#endif
scanline[(scancol<<1)+(scanidx<<1)+0] = aux;
scanline[(scancol<<1)+(scanidx<<1)+1] = mbd;
++scanidx;
}
#if VIDEO_TRACING
if (video_trace_fp && (frameBegin <= frameCount && frameCount <= frameEnd)) {
char buf[16] = { 0 };
uint8_t c = keys_apple2ASCII(mbd, NULL);
if (c <= 0x1F || c >= 0x7F) {
c = ' ';
}
snprintf(buf, sizeof(buf), "%c", c);
fprintf(video_trace_fp, "%03u %s %04X/0:%02X:%s ", vCount, type, addr, mbd, buf);
c = keys_apple2ASCII(aux, NULL);
if (c <= 0x1F || c >= 0x7F) {
c = ' ';
}
snprintf(buf, sizeof(buf), "%c", c);
fprintf(video_trace_fp, "/1:%02X:%s (%lu) ", aux, buf, frameCount);
vm_printSoftwitches(video_trace_fp, /*output_mem:*/false, /*output_pseudo:*/false);
fprintf(video_trace_fp, "%s", "\n");
}
#endif
++hCount;
if (hCount == CYCLES_SCANLINE) {
if (vCount < SCANLINES_VBL_BEGIN) {
// complete scanline flush ...
unsigned int scanend = scancol+scanidx;
assert(scanend == CYCLES_VIS);
_flushScanline(scanline, /*scanrow:*/vCount, scancol, scanend);
}
// begin new scanline ...
hCount = 0;
++vCount;
if (vCount == SCANLINES_FRAME) {
// begin new frame ...
assert(cyclesFrameLast >= CYCLES_FRAME);
cyclesFrameLast -= CYCLES_FRAME;
cycles_video_frame -= CYCLES_FRAME;
vCount = 0;
video_clearDirty(A2_DIRTY_FLAG);
static uint8_t textFlashCounter = 0;
textFlashCounter = (textFlashCounter+1) & 0xf;
if (!textFlashCounter) {
video_flashText();
}
// TODO FIXME : modularize these (and moar) handlers for video frame completion
MB_EndOfVideoFrame();
// UtAIIe 3-17 :
// - keyboard auto-repeat ...
// - power-up reset timing ...
currentBackend->frameComplete();
#if VIDEO_TRACING
++frameCount;
#endif
}
scancol = 0;
scanidx = 0;
mode = _currentMode(vCount);
}
}
if ((scanidx > 0) && (vCount < SCANLINES_VBL_BEGIN)) {
// incomplete scanline flush ...
unsigned int scanend = scancol+scanidx;
_flushScanline(scanline, /*scanrow:*/vCount, scancol, scanend);
scancol = scanend;
scanidx = 0;
}
}
uint16_t video_scannerAddress(bool *ptrIsVBL) {
// get video scanner read position
timing_checkpointCycles();
unsigned int hCount = cycles_video_frame % CYCLES_SCANLINE;
unsigned int vCount = (cycles_video_frame / CYCLES_SCANLINE) % SCANLINES_FRAME;
if (ptrIsVBL) {
*ptrIsVBL = (vCount >= SCANLINES_VBL_BEGIN);
}
// AppleWin : Required for ANSI STORY (end credits) vert scrolling mid-scanline mixed mode: DGR80, TEXT80, DGR80
hCount -= 2;
if ((int)hCount < 0) {
hCount += CYCLES_SCANLINE;
--vCount;
if ((int)vCount < 0) {
vCount = SCANLINES_FRAME-1;
}
}
int page = video_currentPage(run_args.softswitches);
drawpage_mode_t mode = (vCount < SCANLINES_VIS) ? video_currentMainMode(run_args.softswitches) : video_currentMixedMode(run_args.softswitches);
uint16_t addr = _getScannerAddress(mode, page, vCount, hCount);
return addr;
}
uint8_t floating_bus(void) {
uint16_t scanner_addr = video_scannerAddress(NULL);
return apple_ii_64k[0][scanner_addr];
}
#if VIDEO_TRACING
void video_scannerTraceBegin(const char *trace_file, unsigned long count) {
if (video_trace_fp) {
video_scannerTraceEnd();
}
if (trace_file) {
video_trace_fp = fopen(trace_file, "w");
frameCount = 0UL;
frameBegin = 0UL;
frameEnd = UINT_MAX;
if (count > 0) {
frameBegin = frameCount+1;
frameEnd = frameCount+count;
}
}
}
void video_scannerTraceEnd(void) {
if (video_trace_fp) {
fflush(video_trace_fp);
fclose(video_trace_fp);
video_trace_fp = NULL;
frameCount = 0UL;
frameBegin = 0UL;
frameEnd = UINT_MAX;
}
}
void video_scannerTraceCheckpoint(void) {
if (video_trace_fp) {
fflush(video_trace_fp);
}
}
bool video_scannerTraceShouldStop(void) {
return frameCount > frameEnd;
}
#endif
// ----------------------------------------------------------------------------
// state save & restore
bool video_saveState(StateHelper_s *helper) {
bool saved = false;
int fd = helper->fd;
do {
uint8_t state = 0x0;
if (!helper->save(fd, &state, 1)) {
break;
}
LOG("SAVE (no-op) video__current_page = %02x", state);
saved = true;
} while (0);
return saved;
}
bool video_loadState(StateHelper_s *helper) {
bool loaded = false;
int fd = helper->fd;
do {
uint8_t state = 0x0;
if (!helper->load(fd, &state, 1)) {
break;
}
LOG("LOAD (no-op) video__current_page = %02x", state);
loaded = true;
} while (0);
return loaded;
}
// ----------------------------------------------------------------------------
// Video backend registration and selection
void video_registerBackend(video_backend_s *backend, long order) {
assert(!video_initialized); // backends cannot be registered after we've picked one to use
backend_node_s *node = MALLOC(sizeof(backend_node_s));
assert(node);
node->next = NULL;
node->order = order;
node->backend = backend;
backend_node_s *p0 = NULL;
backend_node_s *p = head;
while (p && (order > p->order)) {
p0 = p;
p = p->next;
}
if (p0) {
p0->next = node;
} else {
head = node;
}
node->next = p;
currentBackend = head->backend;
}
void video_printBackends(FILE *out) {
backend_node_s *p = head;
int count = 0;
while (p) {
const char *name = p->backend->name();
if (count++) {
fprintf(out, "|");
}
fprintf(out, "%s", name);
p = p->next;
}
}
static const char *_null_backend_name(void);
void video_chooseBackend(const char *name) {
if (!name) {
name = _null_backend_name();
}
backend_node_s *p = head;
while (p) {
const char *bname = p->backend->name();
if (strcasecmp(name, bname) == 0) {
currentBackend = p->backend;
LOG("Setting current video backend to %s", name);
break;
}
p = p->next;
}
}
video_animation_s *video_getAnimationDriver(void) {
return currentBackend->anim;
}
video_backend_s *video_getCurrentBackend(void) {
return currentBackend;
}
// ----------------------------------------------------------------------------
// NULL video backend ...
static const char *_null_backend_name(void) {
return "none";
}
static void _null_backend_init(void *context) {
}
static void _null_backend_main_loop(void) {
while (null_backend_running) {
sleep(1);
}
}
static void _null_backend_render(void) {
}
static void _null_backend_shutdown(void) {
null_backend_running = false;
}
#if INTERFACE_CLASSIC
static void _null_backend_plotChar(const uint8_t col, const uint8_t row, const interface_colorscheme_t cs, const uint8_t c) {
}
static void _null_backend_plotLine(const uint8_t col, const uint8_t row, const interface_colorscheme_t cs, const char *message) {
}
#endif
static __attribute__((constructor)) void _init_video(void) {
static video_backend_s null_backend = { 0 };
null_backend.name = &_null_backend_name;
null_backend.init = &_null_backend_init;
null_backend.main_loop = &_null_backend_main_loop;
null_backend.render = &_null_backend_render;
null_backend.shutdown = &_null_backend_shutdown;
#if INTERFACE_CLASSIC
null_backend.plotChar = &_null_backend_plotChar;
null_backend.plotLine = &_null_backend_plotLine;
#endif
// Allow headless testing ...
null_backend.flashText = &display_flashText;
null_backend.flushScanline = &display_flushScanline;
null_backend.frameComplete = &display_frameComplete;
static video_animation_s _null_animations = { 0 };
null_backend.anim = &_null_animations;
video_registerBackend(&null_backend, VID_PRIO_NULL);
}