1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-09 06:29:33 +00:00

Merge pull request #17 from TomHarte/2600Accuracy

Made an attempt substantially to improve Atari 2600 emulation accuracy
This commit is contained in:
Thomas Harte 2016-06-03 20:16:04 -04:00
commit 4a507b375b
25 changed files with 1021 additions and 460 deletions

View File

@ -11,7 +11,9 @@
#include <stdio.h>
using namespace Atari2600;
static const int horizontalTimerReload = 227;
namespace {
static const unsigned int horizontalTimerPeriod = 228;
}
Machine::Machine() :
_horizontalTimer(0),
@ -19,12 +21,39 @@ Machine::Machine() :
_lastOutputState(OutputState::Sync),
_piaTimerStatus(0xff),
_rom(nullptr),
_hMoveWillCount(false),
_piaDataValue{0xff, 0xff},
_tiaInputValue{0xff, 0xff}
_tiaInputValue{0xff, 0xff},
_upcomingEventsPointer(0),
_objectCounterPointer(0),
_stateByTime(_stateByExtendTime[0]),
_cycles_since_speaker_update(0)
{
memset(_collisions, 0xff, sizeof(_collisions));
set_reset_line(true);
setup_reported_collisions();
for(int vbextend = 0; vbextend < 2; vbextend++)
{
for(int c = 0; c < 57; c++)
{
OutputState state;
// determine which output state will be active in four cycles from now
switch(c)
{
case 0: case 1: case 2: case 3: state = OutputState::Blank; break;
case 4: case 5: case 6: case 7: state = OutputState::Sync; break;
case 8: case 9: case 10: case 11: state = OutputState::ColourBurst; break;
case 12: case 13: case 14:
case 15: case 16: state = OutputState::Blank; break;
case 17: case 18: state = vbextend ? OutputState::Blank : OutputState::Pixel; break;
default: state = OutputState::Pixel; break;
}
_stateByExtendTime[vbextend][c] = state;
}
}
}
void Machine::setup_output(float aspect_ratio)
@ -43,6 +72,8 @@ void Machine::setup_output(float aspect_ratio)
"return (float(y) / 14.0) * (1.0 - amplitude) + step(1, iPhase) * amplitude * cos(phase + phaseOffset);"
"}");
_crt->set_output_device(Outputs::CRT::Television);
_speaker.set_input_rate(1194720 / 38);
}
void Machine::switch_region()
@ -61,6 +92,8 @@ void Machine::switch_region()
"return (float(y) / 14.0) * (1.0 - amplitude) + step(4, (iPhase + 2u) & 15u) * amplitude * cos(phase + phaseOffset);"
"}");
_crt->set_new_timing(228, 312, Outputs::CRT::ColourSpace::YUV, 228, 1);
// _speaker.set_input_rate(2 * 312 * 50);
}
void Machine::close_output()
@ -75,96 +108,133 @@ Machine::~Machine()
close_output();
}
void Machine::get_output_pixel(uint8_t *pixel, int offset)
void Machine::update_timers(int mask)
{
// get the playfield pixel and hence a proposed colour
uint8_t playfieldPixel = _playfield[offset >> 2];
uint8_t playfieldColour = ((_playfieldControl&6) == 2) ? _playerColour[offset / 80] : _playfieldColour;
unsigned int upcomingPointerPlus4 = (_upcomingEventsPointer + 4)%number_of_upcoming_events;
// get player and missile proposed pixels
uint8_t playerPixels[2] = {0, 0}, missilePixels[2] = {0, 0};
for(int c = 0; c < 2; c++)
_objectCounterPointer = (_objectCounterPointer + 1)%number_of_recorded_counters;
ObjectCounter *oneClockAgo = _objectCounter[(_objectCounterPointer - 1 + number_of_recorded_counters)%number_of_recorded_counters];
ObjectCounter *twoClocksAgo = _objectCounter[(_objectCounterPointer - 2 + number_of_recorded_counters)%number_of_recorded_counters];
ObjectCounter *now = _objectCounter[_objectCounterPointer];
// grab the background now, for application in four clocks
if(mask & (1 << 5) && !(_horizontalTimer&3))
{
const uint8_t repeatMask = _playerAndMissileSize[c]&7;
if(_playerGraphics[c]) {
// figure out player colour
int flipMask = (_playerReflection[c]&0x8) ? 0 : 7;
int relativeTimer = _objectCounter[c] - 5;
switch (repeatMask)
{
case 0: break;
default:
if(repeatMask&4 && relativeTimer >= 64) relativeTimer -= 64;
else if(repeatMask&2 && relativeTimer >= 32) relativeTimer -= 32;
else if(repeatMask&1 && relativeTimer >= 16) relativeTimer -= 16;
break;
case 5:
relativeTimer >>= 1;
break;
case 7:
relativeTimer >>= 2;
break;
}
if(relativeTimer >= 0 && relativeTimer < 8)
playerPixels[c] = (_playerGraphics[c] >> (relativeTimer ^ flipMask)) &1;
}
// figure out missile colour
if((_missileGraphicsEnable[c]&2) && !(_missileGraphicsReset[c]&2)) {
int missileIndex = _objectCounter[2+c] - 4;
switch (repeatMask)
{
case 0: break;
default:
if(repeatMask&4 && missileIndex >= 64) missileIndex -= 64;
else if(repeatMask&2 && missileIndex >= 32) missileIndex -= 32;
else if(repeatMask&1 && missileIndex >= 16) missileIndex -= 16;
break;
case 5:
missileIndex >>= 1;
break;
case 7:
missileIndex >>= 2;
break;
}
int missileSize = 1 << ((_playerAndMissileSize[c] >> 4)&3);
missilePixels[c] = (missileIndex >= 0 && missileIndex < missileSize) ? 1 : 0;
}
unsigned int offset = 4 + _horizontalTimer - (horizontalTimerPeriod - 160);
_upcomingEvents[upcomingPointerPlus4].updates |= Event::Action::Playfield;
_upcomingEvents[upcomingPointerPlus4].playfieldPixel = _playfield[(offset >> 2)%40];
}
// get the ball proposed colour
if(mask & (1 << 4))
{
// the ball becomes visible whenever it hits zero, regardless of whether its status
// is the result of a counter rollover or a programmatic reset, and there's a four
// clock delay on that triggering the start signal
now[4].count = (oneClockAgo[4].count + 1)%160;
now[4].pixel = oneClockAgo[4].pixel + 1;
if(!now[4].count) now[4].pixel = 0;
}
else
{
now[4] = oneClockAgo[4];
}
// check for player and missle triggers
for(int c = 0; c < 4; c++)
{
if(mask & (1 << c))
{
// update the count
now[c].count = (oneClockAgo[c].count + 1)%160;
uint8_t repeatMask = _playerAndMissileSize[c&1] & 7;
ObjectCounter *rollover;
ObjectCounter *equality;
if(c < 2)
{
// update the pixel
now[c].broad_pixel = oneClockAgo[c].broad_pixel + 1;
switch(repeatMask)
{
default: now[c].pixel = oneClockAgo[c].pixel + 1; break;
case 5: now[c].pixel = oneClockAgo[c].pixel + (now[c].broad_pixel&1); break;
case 7: now[c].pixel = oneClockAgo[c].pixel + (((now[c].broad_pixel | (now[c].broad_pixel >> 1))^1)&1); break;
}
// check for a rollover six clocks ago or equality five clocks ago
rollover = twoClocksAgo;
equality = oneClockAgo;
}
else
{
// update the pixel
now[c].pixel = oneClockAgo[c].pixel + 1;
// check for a rollover five clocks ago or equality four clocks ago
rollover = oneClockAgo;
equality = now;
}
if(
(rollover[c].count == 159) ||
(_hasSecondCopy[c&1] && equality[c].count == 16) ||
(_hasThirdCopy[c&1] && equality[c].count == 32) ||
(_hasFourthCopy[c&1] && equality[c].count == 64)
)
{
now[c].pixel = 0;
now[c].broad_pixel = 0;
}
}
else
{
now[c] = oneClockAgo[c];
}
}
}
uint8_t Machine::get_output_pixel()
{
ObjectCounter *now = _objectCounter[_objectCounterPointer];
// get the playfield pixel
unsigned int offset = _horizontalTimer - (horizontalTimerPeriod - 160);
uint8_t playfieldColour = ((_playfieldControl&6) == 2) ? _playerColour[offset / 80] : _playfieldColour;
// ball pixel
uint8_t ballPixel = 0;
if(_ballGraphicsEnable&2) {
int ballIndex = _objectCounter[4] - 4;
int ballSize = 1 << ((_playfieldControl >> 4)&3);
ballPixel = (ballIndex >= 0 && ballIndex < ballSize) ? 1 : 0;
if(now[4].pixel < _ballSize) {
ballPixel = _ballGraphicsEnable[_ballGraphicsSelector];
}
// determine the player and missile pixels
uint8_t playerPixels[2] = { 0, 0 };
uint8_t missilePixels[2] = { 0, 0 };
for(int c = 0; c < 2; c++)
{
if(_playerGraphics[c] && now[c].pixel < 8) {
playerPixels[c] = (_playerGraphics[_playerGraphicsSelector[c]][c] >> (now[c].pixel ^ _playerReflectionMask[c])) & 1;
}
if(!_missileGraphicsReset[c] && now[c+2].pixel < _missileSize[c]) {
missilePixels[c] = _missileGraphicsEnable[c];
}
}
// accumulate collisions
if(playerPixels[0] | playerPixels[1]) {
_collisions[0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6);
_collisions[1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6);
_collisions[2] |= ((playfieldPixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6);
_collisions[3] |= ((playfieldPixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6);
_collisions[7] |= ((playerPixels[0] & playerPixels[1]) << 7);
}
if(playfieldPixel | ballPixel) {
_collisions[4] |= ((playfieldPixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
_collisions[5] |= ((playfieldPixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
_collisions[6] |= ((playfieldPixel & ballPixel) << 7);
}
if(missilePixels[0] & missilePixels[1])
_collisions[7] |= (1 << 6);
int pixel_mask = playerPixels[0] | (playerPixels[1] << 1) | (missilePixels[0] << 2) | (missilePixels[1] << 3) | (ballPixel << 4) | (_playfieldOutput << 5);
_collisions[0] |= _reportedCollisions[pixel_mask][0];
_collisions[1] |= _reportedCollisions[pixel_mask][1];
_collisions[2] |= _reportedCollisions[pixel_mask][2];
_collisions[3] |= _reportedCollisions[pixel_mask][3];
_collisions[4] |= _reportedCollisions[pixel_mask][4];
_collisions[5] |= _reportedCollisions[pixel_mask][5];
_collisions[6] |= _reportedCollisions[pixel_mask][6];
_collisions[7] |= _reportedCollisions[pixel_mask][7];
// apply appropriate priority to pick a colour
playfieldPixel |= ballPixel;
uint8_t playfieldPixel = _playfieldOutput | ballPixel;
uint8_t outputColour = playfieldPixel ? playfieldColour : _backgroundColour;
if(!(_playfieldControl&0x04) || !playfieldPixel) {
@ -172,73 +242,131 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
if(playerPixels[0] || missilePixels[0]) outputColour = _playerColour[0];
}
// store colour
// static int lc;
// if(_vSyncEnabled) lc = 0; else lc += (offset == 159) ? 1 : 0;
// *pixel = (uint8_t)(((offset / 10) << 4) | (((lc >> 4)&7) << 1));
*pixel = outputColour;
// return colour
return outputColour;
}
// in imputing the knowledge that all we're dealing with is the rollover from 159 to 0,
// this is faster than the straightforward +1)%160 per profiling
#define increment_object_counter(c) _objectCounter[c] = (_objectCounter[c]+1)&~((158-_objectCounter[c]) >> 8)
void Machine::setup_reported_collisions()
{
for(int c = 0; c < 64; c++)
{
memset(_reportedCollisions[c], 0, 8);
int playerPixels[2] = { c&1, (c >> 1)&1 };
int missilePixels[2] = { (c >> 2)&1, (c >> 3)&1 };
int ballPixel = (c >> 4)&1;
int playfieldPixel = (c >> 5)&1;
if(playerPixels[0] | playerPixels[1]) {
_reportedCollisions[c][0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6);
_reportedCollisions[c][1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6);
_reportedCollisions[c][2] |= ((playfieldPixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6);
_reportedCollisions[c][3] |= ((playfieldPixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6);
_reportedCollisions[c][7] |= ((playerPixels[0] & playerPixels[1]) << 7);
}
if(playfieldPixel | ballPixel) {
_reportedCollisions[c][4] |= ((playfieldPixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
_reportedCollisions[c][5] |= ((playfieldPixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
_reportedCollisions[c][6] |= ((playfieldPixel & ballPixel) << 7);
}
if(missilePixels[0] & missilePixels[1])
_reportedCollisions[c][7] |= (1 << 6);
}
}
void Machine::output_pixels(unsigned int count)
{
const int32_t start_of_sync = 214;
const int32_t end_of_sync = 198;
const int32_t end_of_colour_burst = 188;
while(count--)
{
OutputState state;
if(_upcomingEvents[_upcomingEventsPointer].updates)
{
// apply any queued changes and flush the record
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveSetup)
{
// schedule an extended left border
_stateByTime = _stateByExtendTime[1];
// update hmove
if(!(_horizontalTimer&3)) {
// clear any ongoing moves
if(_hMoveFlags)
{
for(int c = 0; c < number_of_upcoming_events; c++)
{
_upcomingEvents[c].updates &= ~(Event::Action::HMoveCompare | Event::Action::HMoveDecrement);
}
}
if(_hMoveFlags) {
const uint8_t counterValue = _hMoveCounter ^ 0x7;
for(int c = 0; c < 5; c++) {
if(counterValue == (_objectMotion[c] >> 4)) _hMoveFlags &= ~(1 << c);
if(_hMoveFlags&(1 << c)) increment_object_counter(c);
// schedule new moves
_hMoveFlags = 0x1f;
_hMoveCounter = 15;
// follow-through into a compare immediately
_upcomingEvents[_upcomingEventsPointer].updates |= Event::Action::HMoveCompare;
}
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveCompare)
{
for(int c = 0; c < 5; c++)
{
if(((_objectMotion[c] >> 4)^_hMoveCounter) == 7)
{
_hMoveFlags &= ~(1 << c);
}
}
if(_hMoveFlags)
{
if(_hMoveCounter) _hMoveCounter--;
_upcomingEvents[(_upcomingEventsPointer+4)%number_of_upcoming_events].updates |= Event::Action::HMoveCompare;
_upcomingEvents[(_upcomingEventsPointer+2)%number_of_upcoming_events].updates |= Event::Action::HMoveDecrement;
}
}
if(_hMoveIsCounting) {
_hMoveIsCounting = !!_hMoveCounter;
_hMoveCounter = (_hMoveCounter-1)&0xf;
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveDecrement)
{
update_timers(_hMoveFlags);
}
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::ResetCounter)
{
_objectCounter[_objectCounterPointer][_upcomingEvents[_upcomingEventsPointer].counter].count = 0;
}
// zero out current update event
_upcomingEvents[_upcomingEventsPointer].updates = 0;
}
// progress to next event
_upcomingEventsPointer = (_upcomingEventsPointer + 1)%number_of_upcoming_events;
// blank is decoded as 68 counts; sync and colour burst as 16 counts
// determine which output state is currently active
OutputState primary_state = _stateByTime[_horizontalTimer >> 2];
OutputState effective_state = primary_state;
// 4 blank
// 4 sync
// 9 'blank'; colour burst after 4
// 40 pixels
// update pixel timers
if(primary_state == OutputState::Pixel) update_timers(~0);
// it'll be about 43 cycles from start of hsync to start of visible frame, so...
// guesses, until I can find information: 26 cycles blank, 16 sync, 40 blank, 160 pixels
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
if(_vBlankEnabled) {
state = OutputState::Blank;
} else {
state = OutputState::Pixel;
}
// update the background chain
if(_horizontalTimer >= 64 && _horizontalTimer <= 160+64 && !(_horizontalTimer&3))
{
_playfieldOutput = _nextPlayfieldOutput;
_nextPlayfieldOutput = _playfield[(_horizontalTimer - 64) >> 2];
}
else if(_horizontalTimer < end_of_colour_burst) state = OutputState::Blank;
else if(_horizontalTimer < end_of_sync) state = OutputState::ColourBurst;
else if(_horizontalTimer < start_of_sync) state = OutputState::Sync;
else state = OutputState::Blank;
// logic: if vsync is enabled, output the opposite of the automatic hsync output
// if vsync is enabled, output the opposite of the automatic hsync output;
// also honour the vertical blank flag
if(_vSyncEnabled) {
state = (state = OutputState::Sync) ? OutputState::Blank : OutputState::Sync;
effective_state = (effective_state = OutputState::Sync) ? OutputState::Blank : OutputState::Sync;
} else if(_vBlankEnabled && effective_state == OutputState::Pixel) {
effective_state = OutputState::Blank;
}
// decide what that means needs to be communicated to the CRT
_lastOutputStateDuration++;
if(state != _lastOutputState) {
if(effective_state != _lastOutputState) {
switch(_lastOutputState) {
case OutputState::Blank: _crt->output_blank(_lastOutputStateDuration); break;
case OutputState::Sync: _crt->output_sync(_lastOutputStateDuration); break;
@ -246,35 +374,34 @@ void Machine::output_pixels(unsigned int count)
case OutputState::Pixel: _crt->output_data(_lastOutputStateDuration, 1); break;
}
_lastOutputStateDuration = 0;
_lastOutputState = state;
_lastOutputState = effective_state;
if(state == OutputState::Pixel) {
if(effective_state == OutputState::Pixel) {
_outputBuffer = _crt->allocate_write_area(160);
} else {
_outputBuffer = nullptr;
}
}
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
uint8_t throwaway_pixel;
get_output_pixel(_outputBuffer ? &_outputBuffer[_lastOutputStateDuration] : &throwaway_pixel, 159 - _horizontalTimer);
// increment all graphics counters
increment_object_counter(0);
increment_object_counter(1);
increment_object_counter(2);
increment_object_counter(3);
increment_object_counter(4);
// decide on a pixel colour if that's what's happening
if(effective_state == OutputState::Pixel)
{
uint8_t colour = get_output_pixel();
if(_outputBuffer)
{
*_outputBuffer = colour;
_outputBuffer++;
}
}
// assumption here: signed shifts right; otherwise it's just
// an attempt to avoid both the % operator and a conditional
_horizontalTimer--;
const int32_t sign_extension = _horizontalTimer >> 31;
_horizontalTimer = (_horizontalTimer&~sign_extension) | (sign_extension&horizontalTimerReload);
// advance horizontal timer, perform reset actions if desired
_horizontalTimer = (_horizontalTimer + 1) % horizontalTimerPeriod;
if(!_horizontalTimer)
_vBlankExtend = false;
{
// switch back to a normal length left border
_stateByTime = _stateByExtendTime[0];
set_ready_line(false);
}
}
}
@ -283,26 +410,19 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
set_reset_line(false);
uint8_t returnValue = 0xff;
unsigned int cycles_run_for = 1;
const int32_t ready_line_disable_time = 227;//horizontalTimerReload;
unsigned int cycles_run_for = 3;
// this occurs as a feedback loop — the 2600 requests ready, then performs the cycles_run_for
// leap to the end of ready only once ready is signalled — because on a 6502 ready doesn't take
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// skips to the end of the line.
if(operation == CPU6502::BusOperation::Ready) {
unsigned int distance_to_end_of_ready = (_horizontalTimer - ready_line_disable_time + horizontalTimerReload + 1)%(horizontalTimerReload + 1);
cycles_run_for = distance_to_end_of_ready / 3;
output_pixels(distance_to_end_of_ready);
} else {
output_pixels(3);
unsigned int distance_to_end_of_ready = horizontalTimerPeriod - _horizontalTimer;
cycles_run_for = distance_to_end_of_ready;
}
if(_hMoveWillCount) {
_hMoveCounter = 0x0f;
_hMoveFlags = 0x1f;
_hMoveIsCounting = true;
_hMoveWillCount = false;
}
if(_horizontalTimer == ready_line_disable_time)
set_ready_line(false);
output_pixels(cycles_run_for);
_cycles_since_speaker_update += cycles_run_for;
if(operation != CPU6502::BusOperation::Ready) {
@ -378,14 +498,26 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x01: _vBlankEnabled = !!(*value & 0x02); break;
case 0x02:
set_ready_line(true);
if(_horizontalTimer) set_ready_line(true);
break;
case 0x03:
_horizontalTimer = 0;
// Reset is delayed by four cycles.
_horizontalTimer = horizontalTimerPeriod - 4;
// TODO: audio will now be out of synchronisation — fix
break;
case 0x04:
case 0x05: _playerAndMissileSize[decodedAddress - 0x04] = *value; break;
case 0x05: {
int entry = decodedAddress - 0x04;
_playerAndMissileSize[entry] = *value;
_missileSize[entry] = 1 << ((*value >> 4)&3);
uint8_t repeatMask = (*value)&7;
_hasSecondCopy[entry] = (repeatMask == 1) || (repeatMask == 3);
_hasThirdCopy[entry] = (repeatMask == 2) || (repeatMask == 3) || (repeatMask == 6);
_hasFourthCopy[entry] = (repeatMask == 4) || (repeatMask == 6);
} break;
case 0x06:
case 0x07: _playerColour[decodedAddress - 0x06] = *value; break;
@ -395,6 +527,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x0a: {
uint8_t old_playfield_control = _playfieldControl;
_playfieldControl = *value;
_ballSize = 1 << ((_playfieldControl >> 4)&3);
// did the mirroring bit change?
if((_playfieldControl^old_playfield_control)&1) {
@ -406,7 +539,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
} break;
case 0x0b:
case 0x0c: _playerReflection[decodedAddress - 0x0b] = *value; break;
case 0x0c: _playerReflectionMask[decodedAddress - 0x0b] = (*value)&8 ? 0 : 7; break;
case 0x0d:
_playfield[0] = ((*value) >> 4)&1;
@ -454,23 +587,40 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
break;
case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14: _objectCounter[decodedAddress - 0x10] = 0; break;
case 0x14:
_upcomingEvents[(_upcomingEventsPointer + 4)%number_of_upcoming_events].updates |= Event::Action::ResetCounter;
_upcomingEvents[(_upcomingEventsPointer + 4)%number_of_upcoming_events].counter = decodedAddress - 0x10;
break;
case 0x15: case 0x16:
update_audio();
_speaker.set_control(decodedAddress - 0x15, *value);
break;
case 0x17: case 0x18:
update_audio();
_speaker.set_divider(decodedAddress - 0x17, *value);
break;
case 0x19: case 0x1a:
update_audio();
_speaker.set_volume(decodedAddress - 0x19, *value);
break;
case 0x1c:
_ballGraphicsEnable = _ballGraphicsEnableLatch;
_ballGraphicsEnable[1] = _ballGraphicsEnable[0];
case 0x1b: {
int index = decodedAddress - 0x1b;
_playerGraphicsLatch[index] = *value;
if(!(_playerGraphicsLatchEnable[index]&1))
_playerGraphics[index] = _playerGraphicsLatch[index];
_playerGraphics[index^1] = _playerGraphicsLatch[index^1];
_playerGraphics[0][index] = *value;
_playerGraphics[1][index^1] = _playerGraphics[0][index^1];
} break;
case 0x1d: _missileGraphicsEnable[0] = *value; break;
case 0x1e: _missileGraphicsEnable[1] = *value; break;
case 0x1d:
case 0x1e:
_missileGraphicsEnable[decodedAddress - 0x1d] = ((*value) >> 1)&1;
// printf("e:%02x <- %c\n", decodedAddress - 0x1d, ((*value)&1) ? 'E' : '-');
break;
case 0x1f:
_ballGraphicsEnableLatch = *value;
if(!(_ballGraphicsEnableDelay&1))
_ballGraphicsEnable = _ballGraphicsEnableLatch;
_ballGraphicsEnable[0] = ((*value) >> 1)&1;
break;
case 0x20:
@ -481,21 +631,42 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
_objectMotion[decodedAddress - 0x20] = *value;
break;
case 0x25: _playerGraphicsLatchEnable[0] = *value; break;
case 0x26: _playerGraphicsLatchEnable[1] = *value; break;
case 0x27: _ballGraphicsEnableDelay = *value; break;
case 0x25: _playerGraphicsSelector[0] = (*value)&1; break;
case 0x26: _playerGraphicsSelector[1] = (*value)&1; break;
case 0x27: _ballGraphicsSelector = (*value)&1; break;
case 0x28:
case 0x29:
if(!(*value&0x02) && _missileGraphicsReset[decodedAddress - 0x28]&0x02)
_objectCounter[decodedAddress - 0x26] = _objectCounter[decodedAddress - 0x28]; // TODO: +3 for normal, +6 for double, +10 for quad
_missileGraphicsReset[decodedAddress - 0x28] = *value;
{
// TODO: this should properly mean setting a flag and propagating later, I think?
int index = decodedAddress - 0x28;
if(!(*value&0x02) && _missileGraphicsReset[index])
{
_objectCounter[_objectCounterPointer][index + 2].count = _objectCounter[_objectCounterPointer][index].count;
uint8_t repeatMask = _playerAndMissileSize[index] & 7;
int extra_offset;
switch(repeatMask)
{
default: extra_offset = 3; break;
case 5: extra_offset = 6; break;
case 7: extra_offset = 10; break;
}
_objectCounter[_objectCounterPointer][index + 2].count = (_objectCounter[_objectCounterPointer][index + 2].count + extra_offset)%160;
}
_missileGraphicsReset[index] = !!((*value) & 0x02);
// printf("r:%02x <- %c\n", decodedAddress - 0x28, ((*value)&2) ? 'R' : '-');
}
break;
case 0x2a:
_vBlankExtend = true;
_hMoveWillCount = true;
break;
case 0x2a: {
// justification for +5: "we need to wait at least 71 [clocks] before the HMOVE operation is complete";
// which will take 16*4 + 2 = 66 cycles from the first compare, implying the first compare must be
// in five cycles from now
// int start_pause = ((_horizontalTimer + 3)&3) + 4;
_upcomingEvents[(_upcomingEventsPointer + 5)%number_of_upcoming_events].updates |= Event::Action::HMoveSetup;
} break;
case 0x2b:
_objectMotion[0] =
_objectMotion[1] =
@ -525,8 +696,10 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x01:
case 0x03:
// TODO: port DDR
printf("!!!DDR!!!");
break;
case 0x04:
case 0x06:
returnValue &= _piaTimerValue >> _piaTimerShift;
if(_writtenPiaTimerShift != _piaTimerShift) {
@ -535,8 +708,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
break;
case 0x05:
case 0x07:
returnValue &= _piaTimerStatus;
_piaTimerStatus &= ~0x40;
_piaTimerStatus &= ~0x80;
break;
}
} else {
@ -546,9 +720,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x05:
case 0x06:
case 0x07:
_writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07);
_piaTimerValue = (unsigned int)(*value << _piaTimerShift);
_piaTimerStatus &= ~0xc0;
_writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07); // i.e. 0, 3, 6, 10
_piaTimerValue = (unsigned int)(*value) << _piaTimerShift;
_piaTimerStatus &= ~0x40;
break;
}
}
@ -559,15 +733,26 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
}
if(_piaTimerValue >= cycles_run_for) {
_piaTimerValue -= cycles_run_for;
if(_piaTimerValue >= cycles_run_for / 3) {
_piaTimerValue -= cycles_run_for / 3;
} else {
_piaTimerValue += 0xff - cycles_run_for;
_piaTimerValue = 0x100 + ((_piaTimerValue - (cycles_run_for / 3)) >> _piaTimerShift);
_piaTimerShift = 0;
_piaTimerStatus |= 0xc0;
}
return cycles_run_for;
// static unsigned int total_cycles = 0;
// total_cycles += cycles_run_for / 3;
// static time_t logged_time = 0;
// time_t time_now = time(nullptr);
// if(time_now - logged_time > 0)
// {
// printf("[c] %ld : %d\n", time_now - logged_time, total_cycles);
// total_cycles = 0;
// logged_time = time_now;
// }
return cycles_run_for / 3;
}
void Machine::set_digital_input(Atari2600DigitalInput input, bool state)
@ -615,3 +800,159 @@ void Machine::set_rom(size_t length, const uint8_t *data)
_romPages[2] = &_rom[2048 & romMask];
_romPages[3] = &_rom[3072 & romMask];
}
#pragma mark - Audio
void Machine::update_audio()
{
unsigned int audio_cycles = _cycles_since_speaker_update / 114;
// static unsigned int total_cycles = 0;
// total_cycles += audio_cycles;
// static time_t logged_time = 0;
// time_t time_now = time(nullptr);
// if(time_now - logged_time > 0)
// {
// printf("[s] %ld : %d\n", time_now - logged_time, total_cycles);
// total_cycles = 0;
// logged_time = time_now;
// }
_speaker.run_for_cycles(audio_cycles);
_cycles_since_speaker_update %= 114;
}
void Machine::synchronise()
{
update_audio();
}
Atari2600::Speaker::Speaker()
{
_poly4_counter[0] = _poly4_counter[1] = 0x00f;
_poly5_counter[0] = _poly5_counter[1] = 0x01f;
_poly9_counter[0] = _poly9_counter[1] = 0x1ff;
}
Atari2600::Speaker::~Speaker()
{
}
void Atari2600::Speaker::set_volume(int channel, uint8_t volume)
{
_volume[channel] = volume & 0xf;
}
void Atari2600::Speaker::set_divider(int channel, uint8_t divider)
{
_divider[channel] = divider & 0x1f;
_divider_counter[channel] = 0;
}
void Atari2600::Speaker::set_control(int channel, uint8_t control)
{
_control[channel] = control & 0xf;
}
#define advance_poly4(c) _poly4_counter[channel] = (_poly4_counter[channel] >> 1) | (((_poly4_counter[channel] << 3) ^ (_poly4_counter[channel] << 2))&0x008)
#define advance_poly5(c) _poly5_counter[channel] = (_poly5_counter[channel] >> 1) | (((_poly5_counter[channel] << 4) ^ (_poly5_counter[channel] << 2))&0x010)
#define advance_poly9(c) _poly9_counter[channel] = (_poly9_counter[channel] >> 1) | (((_poly9_counter[channel] << 4) ^ (_poly9_counter[channel] << 8))&0x100)
void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
{
for(unsigned int c = 0; c < number_of_samples; c++)
{
target[c] = 0;
for(int channel = 0; channel < 2; channel++)
{
_divider_counter[channel] ++;
int level = 0;
switch(_control[channel])
{
case 0x0: case 0xb: // constant 1
level = 1;
break;
case 0x4: case 0x5: // div2 tone
level = (_divider_counter[channel] / (_divider[channel]+1))&1;
break;
case 0xc: case 0xd: // div6 tone
level = (_divider_counter[channel] / ((_divider[channel]+1)*3))&1;
break;
case 0x6: case 0xa: // div31 tone
level = (_divider_counter[channel] / (_divider[channel]+1))%30 <= 18;
break;
case 0xe: // div93 tone
level = (_divider_counter[channel] / ((_divider[channel]+1)*3))%30 <= 18;
break;
case 0x1: // 4-bit poly
level = _poly4_counter[channel]&1;
if(_divider_counter[channel] == _divider[channel]+1)
{
_divider_counter[channel] = 0;
advance_poly4(channel);
}
break;
case 0x2: // 4-bit poly div31
level = _poly4_counter[channel]&1;
if(_divider_counter[channel]%(30*(_divider[channel]+1)) == 18)
{
advance_poly4(channel);
}
break;
case 0x3: // 5/4-bit poly
level = _output_state[channel];
if(_divider_counter[channel] == _divider[channel]+1)
{
if(_poly5_counter[channel]&1)
{
_output_state[channel] = _poly4_counter[channel]&1;
advance_poly4(channel);
}
advance_poly5(channel);
}
break;
case 0x7: case 0x9: // 5-bit poly
level = _poly5_counter[channel]&1;
if(_divider_counter[channel] == _divider[channel]+1)
{
_divider_counter[channel] = 0;
advance_poly5(channel);
}
break;
case 0xf: // 5-bit poly div6
level = _poly5_counter[channel]&1;
if(_divider_counter[channel] == (_divider[channel]+1)*3)
{
_divider_counter[channel] = 0;
advance_poly5(channel);
}
break;
case 0x8: // 9-bit poly
level = _poly9_counter[channel]&1;
if(_divider_counter[channel] == _divider[channel]+1)
{
_divider_counter[channel] = 0;
advance_poly9(channel);
}
break;
}
target[c] += _volume[channel] * 1024 * level;
}
}
}
void Atari2600::Speaker::skip_samples(unsigned int number_of_samples)
{
}

View File

@ -10,28 +10,64 @@
#define Atari2600_cpp
#include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../CRTMachine.hpp"
#include <stdint.h>
#include "Atari2600Inputs.h"
namespace Atari2600 {
class Machine: public CPU6502::Processor<Machine> {
const unsigned int number_of_upcoming_events = 6;
const unsigned int number_of_recorded_counters = 7;
class Speaker: public ::Outputs::Filter<Speaker> {
public:
Speaker();
~Speaker();
void set_volume(int channel, uint8_t volume);
void set_divider(int channel, uint8_t divider);
void set_control(int channel, uint8_t control);
void get_samples(unsigned int number_of_samples, int16_t *target);
void skip_samples(unsigned int number_of_samples);
private:
uint8_t _volume[2];
uint8_t _divider[2];
uint8_t _control[2];
int _poly4_counter[2];
int _poly5_counter[2];
int _poly9_counter[2];
int _output_state[2];
int _divider_counter[2];
int _pattern_periods[16];
int _patterns[16][512];
};
class Machine: public CPU6502::Processor<Machine>, public CRTMachine::Machine {
public:
Machine();
~Machine();
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void set_rom(size_t length, const uint8_t *data);
void switch_region();
void set_digital_input(Atari2600DigitalInput input, bool state);
Outputs::CRT::CRT *get_crt() { return _crt; }
void setup_output(float aspect_ratio);
void close_output();
// to satisfy CPU6502::Processor
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void synchronise();
// to satisfy CRTMachine::Machine
virtual void setup_output(float aspect_ratio);
virtual void close_output();
virtual Outputs::CRT::CRT *get_crt() { return _crt; }
virtual Outputs::Speaker *get_speaker() { return &_speaker; }
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
private:
uint8_t *_rom, *_romPages[4], _ram[128];
@ -46,32 +82,81 @@ class Machine: public CPU6502::Processor<Machine> {
uint8_t _playfieldControl;
uint8_t _playfieldColour;
uint8_t _backgroundColour;
uint8_t _playfield[40];
uint8_t _playfield[41];
// ... and derivatives
int _ballSize, _missileSize[2];
// delayed clock events
enum OutputState {
Sync,
Blank,
ColourBurst,
Pixel
};
struct Event {
enum Action {
Playfield = 1 << 0,
ResetCounter = 1 << 1,
HMoveSetup = 1 << 2,
HMoveCompare = 1 << 3,
HMoveDecrement = 1 << 4,
};
int updates;
OutputState state;
uint8_t playfieldPixel;
int counter;
Event() : updates(0), playfieldPixel(0) {}
} _upcomingEvents[number_of_upcoming_events];
unsigned int _upcomingEventsPointer;
// object counters
struct ObjectCounter {
int count; // the counter value, multiplied by four, counting phase
int pixel; // for non-sprite objects, a count of cycles since the last counter reset; for sprite objects a count of pixels so far elapsed
int broad_pixel; // for sprite objects, a count of cycles since the last counter reset; otherwise unused
ObjectCounter() : count(0), pixel(0), broad_pixel(0) {}
} _objectCounter[number_of_recorded_counters][5];
unsigned int _objectCounterPointer;
// the latched playfield output
uint8_t _playfieldOutput, _nextPlayfieldOutput;
// player registers
uint8_t _playerColour[2];
uint8_t _playerReflection[2];
uint8_t _playerGraphicsLatch[2], _playerGraphics[2];
uint8_t _playerGraphicsLatchEnable[2];
uint8_t _playerReflectionMask[2];
uint8_t _playerGraphics[2][2];
uint8_t _playerGraphicsSelector[2];
bool _playerStart[2];
// object flags
bool _hasSecondCopy[2];
bool _hasThirdCopy[2];
bool _hasFourthCopy[2];
uint8_t _objectMotion[5]; // the value stored to this counter's motion register
// player + missile registers
uint8_t _playerAndMissileSize[2];
// missile registers
uint8_t _missileGraphicsEnable[2], _missileGraphicsReset[2];
uint8_t _missileGraphicsEnable[2];
bool _missileGraphicsReset[2];
// ball registers
uint8_t _ballGraphicsEnable, _ballGraphicsEnableLatch;
uint8_t _ballGraphicsEnableDelay;
uint8_t _ballGraphicsEnable[2];
uint8_t _ballGraphicsSelector;
// graphics output
int32_t _horizontalTimer;
unsigned int _horizontalTimer;
bool _vSyncEnabled, _vBlankEnabled;
bool _vBlankExtend;
// horizontal motion control
uint8_t _hMoveCounter;
bool _hMoveIsCounting, _hMoveWillCount;
uint8_t _objectCounter[5], _objectMotion[5];
uint8_t _hMoveFlags;
// joystick state
@ -82,21 +167,28 @@ class Machine: public CPU6502::Processor<Machine> {
// collisions
uint8_t _collisions[8];
enum OutputState {
Sync,
Blank,
ColourBurst,
Pixel
};
void output_pixels(unsigned int count);
void get_output_pixel(uint8_t *pixel, int offset);
uint8_t get_output_pixel();
void update_timers(int mask);
// outputs
Outputs::CRT::CRT *_crt;
Speaker _speaker;
// speaker backlog accumlation counter
unsigned int _cycles_since_speaker_update;
void update_audio();
// latched output state
unsigned int _lastOutputStateDuration;
OutputState _stateByExtendTime[2][57];
OutputState *_stateByTime;
OutputState _lastOutputState;
uint8_t *_outputBuffer;
// lookup table for collision reporting
uint8_t _reportedCollisions[64][8];
void setup_reported_collisions();
};
}

30
Machines/CRTMachine.hpp Normal file
View File

@ -0,0 +1,30 @@
//
// CRTMachine.hpp
// Clock Signal
//
// Created by Thomas Harte on 31/05/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTMachine_hpp
#define CRTMachine_hpp
#include "../Outputs/CRT/CRT.hpp"
#include "../Outputs/Speaker.hpp"
namespace CRTMachine {
class Machine {
public:
virtual void setup_output(float aspect_ratio) = 0;
virtual void close_output() = 0;
virtual Outputs::CRT::CRT *get_crt() = 0;
virtual Outputs::Speaker *get_speaker() = 0;
virtual void run_for_cycles(int number_of_cycles) = 0;
};
}
#endif /* CRTMachine_hpp */

View File

@ -450,7 +450,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
return cycles;
}
void Machine::update_output()
void Machine::synchronise()
{
update_display();
update_audio();

View File

@ -10,9 +10,8 @@
#define Electron_hpp
#include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../Outputs/Speaker.hpp"
#include "../../Storage/Tape/Tape.hpp"
#include "../CRTMachine.hpp"
#include <stdint.h>
namespace Electron {
@ -142,30 +141,34 @@ class Speaker: public ::Outputs::Filter<Speaker> {
@discussion An instance of Electron::Machine represents the current state of an
Acorn Electron.
*/
class Machine: public CPU6502::Processor<Machine>, Tape::Delegate {
class Machine: public CPU6502::Processor<Machine>, public CRTMachine::Machine, Tape::Delegate {
public:
Machine();
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void set_rom(ROMSlot slot, size_t length, const uint8_t *data);
void set_tape(std::shared_ptr<Storage::Tape> tape);
void set_key_state(Key key, bool isPressed);
void clear_all_keys();
void setup_output(float aspect_ratio);
void close_output();
Outputs::CRT::CRT *get_crt() { return _crt.get(); }
Outputs::Speaker *get_speaker() { return &_speaker; }
virtual void tape_did_change_interrupt_status(Tape *tape);
void update_output();
inline void set_use_fast_tape_hack(bool activate) { _use_fast_tape_hack = activate; }
// to satisfy CPU6502::Processor
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void synchronise();
// to satisfy CRTMachine::Machine
virtual void setup_output(float aspect_ratio);
virtual void close_output();
virtual Outputs::CRT::CRT *get_crt() { return _crt.get(); }
virtual Outputs::Speaker *get_speaker() { return &_speaker; }
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
// to satisfy Tape::Delegate
virtual void tape_did_change_interrupt_status(Tape *tape);
private:
inline void update_display();
@ -215,12 +218,12 @@ class Machine: public CPU6502::Processor<Machine>, Tape::Delegate {
uint8_t *_current_output_target, *_initial_output_target;
unsigned int _current_output_divider;
// Tape.
// Tape
Tape _tape;
bool _use_fast_tape_hack;
bool _fast_load_is_in_data;
// Outputs.
// Outputs
std::unique_ptr<Outputs::CRT::CRT> _crt;
Speaker _speaker;
};

View File

@ -334,6 +334,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
4B046DC31CFE651500E9E45E /* CRTMachine.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTMachine.hpp; sourceTree = "<group>"; };
4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTConstants.hpp; sourceTree = "<group>"; };
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRT.cpp; sourceTree = "<group>"; };
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRT.hpp; sourceTree = "<group>"; };
@ -1192,6 +1193,7 @@
children = (
4B2E2D961C3A06EC00138695 /* Atari2600 */,
4B2E2D9E1C3A070900138695 /* Electron */,
4B046DC31CFE651500E9E45E /* CRTMachine.hpp */,
);
name = Machines;
path = ../../Machines;

View File

@ -10,17 +10,17 @@ import Cocoa
class Atari2600Document: MachineDocument {
private var atari2600 = CSAtari2600()
override func machine() -> CSMachine? {
return atari2600
}
// MARK: NSDocument overrides
override init() {
super.init()
self.intendedCyclesPerSecond = 1194720
}
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
atari2600.setView(openGLView, aspectRatio: 4.0 / 3.0)
}
override class func autosavesInPlace() -> Bool {
return true
}
@ -31,7 +31,6 @@ class Atari2600Document: MachineDocument {
return "Atari2600Document"
}
private var atari2600 = CSAtari2600()
override func dataOfType(typeName: String) throws -> NSData {
// Insert code here to write your document to data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override fileWrapperOfType:error:, writeToURL:ofType:error:, or writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
@ -42,21 +41,6 @@ class Atari2600Document: MachineDocument {
atari2600.setROM(data)
}
override func close() {
super.close()
openGLView.invalidate()
}
// MARK: MachineDocument overrides
override func runForNumberOfCycles(numberOfCycles: Int32) {
atari2600.runForNumberOfCycles(numberOfCycles)
}
override func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
atari2600.drawViewForPixelSize(view.backingSize, onlyIfDirty: onlyIfDirty)
}
// MARK: CSOpenGLViewResponderDelegate
private func inputForKey(event: NSEvent) -> Atari2600DigitalInput? {

View File

@ -12,21 +12,26 @@ import AudioToolbox
class ElectronDocument: MachineDocument {
private lazy var electron = CSElectron()
override func machine() -> CSMachine! {
return electron
}
override func aspectRatio() -> NSSize {
return NSSize(width: 11.0, height: 10.0)
}
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
self.intendedCyclesPerSecond = 2000000
aController.window?.contentAspectRatio = NSSize(width: 11.0, height: 10.0)
openGLView.performWithGLContext({
if let osPath = NSBundle.mainBundle().pathForResource("os", ofType: "rom") {
self.electron.setOSROM(NSData(contentsOfFile: osPath)!)
}
if let basicPath = NSBundle.mainBundle().pathForResource("basic", ofType: "rom") {
self.electron.setBASICROM(NSData(contentsOfFile: basicPath)!)
}
self.electron.setView(self.openGLView, aspectRatio: 11.0 / 10.0)
self.electron.audioQueue = self.audioQueue
})
if let osPath = NSBundle.mainBundle().pathForResource("os", ofType: "rom") {
self.electron.setOSROM(NSData(contentsOfFile: osPath)!)
}
if let basicPath = NSBundle.mainBundle().pathForResource("basic", ofType: "rom") {
self.electron.setBASICROM(NSData(contentsOfFile: basicPath)!)
}
establishStoredOptions()
}
@ -58,19 +63,6 @@ class ElectronDocument: MachineDocument {
electron.setROM(data, slot: 15)
}
lazy var actionLock = NSLock()
lazy var drawLock = NSLock()
override func close() {
actionLock.lock()
drawLock.lock()
openGLView.invalidate()
openGLView.openGLContext!.makeCurrentContext()
actionLock.unlock()
drawLock.unlock()
super.close()
}
// MARK: IBActions
@IBOutlet var displayTypeButton: NSPopUpButton!
@IBAction func setDisplayType(sender: NSPopUpButton!) {
@ -102,21 +94,6 @@ class ElectronDocument: MachineDocument {
self.displayTypeButton.selectItemAtIndex(displayType)
}
// MARK: CSOpenGLViewDelegate
override func runForNumberOfCycles(numberOfCycles: Int32) {
if actionLock.tryLock() {
electron.runForNumberOfCycles(numberOfCycles)
actionLock.unlock()
}
}
override func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
if drawLock.tryLock() {
electron.drawViewForPixelSize(view.backingSize, onlyIfDirty: onlyIfDirty)
drawLock.unlock()
}
}
// MARK: NSWindowDelegate
func windowDidResignKey(notification: NSNotification) {
electron.clearAllKeys()

View File

@ -11,6 +11,16 @@ import AudioToolbox
class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDelegate, NSWindowDelegate {
lazy var actionLock = NSLock()
lazy var drawLock = NSLock()
func machine() -> CSMachine! {
return nil
}
func aspectRatio() -> NSSize {
return NSSize(width: 4.0, height: 3.0)
}
@IBOutlet weak var openGLView: CSOpenGLView! {
didSet {
openGLView.delegate = self
@ -23,13 +33,35 @@ class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDe
optionsPanel?.setIsVisible(true)
}
lazy var audioQueue = AudioQueue()
var audioQueue : AudioQueue! = nil
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
// bind the content aspect ratio to remain 4:3 from now on
aController.window?.contentAspectRatio = NSSize(width: 4.0, height: 3.0)
// establish the output aspect ratio and audio
let displayAspectRatio = self.aspectRatio()
aController.window?.contentAspectRatio = displayAspectRatio
openGLView.performWithGLContext({
self.machine().setView(self.openGLView, aspectRatio: Float(displayAspectRatio.width / displayAspectRatio.height))
})
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
let maximumSamplingRate = AudioQueue.preferredSamplingRate()
let selectedSamplingRate = self.machine().idealSamplingRateFromRange(NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
audioQueue = AudioQueue(samplingRate: Float64(selectedSamplingRate))
self.machine().audioQueue = self.audioQueue
self.machine().setAudioSamplingRate(selectedSamplingRate)
}
override func close() {
actionLock.lock()
drawLock.lock()
openGLView.invalidate()
openGLView.openGLContext!.makeCurrentContext()
actionLock.unlock()
drawLock.unlock()
super.close()
}
var intendedCyclesPerSecond: Int64 = 0
@ -54,16 +86,29 @@ class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDe
skippedFrames = 0
}
if skippedFrames > 4 {
numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency))
}
// run for at most three frames up to and until that causes overshoots in the
// permitted processing window for at least four consecutive frames, in which
// case limit to one
numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency * ((skippedFrames > 4) ? 3.0 : 1.0)))
runForNumberOfCycles(Int32(numberOfCycles))
}
lastTime = time
}
func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {}
func runForNumberOfCycles(numberOfCycles: Int32) {}
// MARK: CSOpenGLViewDelegate
func runForNumberOfCycles(numberOfCycles: Int32) {
if actionLock.tryLock() {
self.machine().runForNumberOfCycles(numberOfCycles)
actionLock.unlock()
}
}
func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
if drawLock.tryLock() {
self.machine().drawViewForPixelSize(view.backingSize, onlyIfDirty: onlyIfDirty)
drawLock.unlock()
}
}
// MARK: CSOpenGLViewResponderDelegate
func keyDown(event: NSEvent) {}

View File

@ -14,6 +14,7 @@
CVDisplayLinkRef _displayLink;
uint32_t _updateIsOngoing;
BOOL _hasSkipped;
dispatch_queue_t _serialDispatchQueue;
}
- (void)prepareOpenGL
@ -33,6 +34,10 @@
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
// create a serial dispatch queue
_serialDispatchQueue = dispatch_queue_create("OpenGLView", DISPATCH_QUEUE_SERIAL);
// dispatch_set_target_queue(_serialDispatchQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
// set the clear colour
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
@ -51,31 +56,25 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency
{
const uint32_t processingMask = 0x01;
const uint32_t drawingMask = 0x02;
// Always post a -openGLView:didUpdateToTime:. This is the hook upon which the substantial processing occurs.
// Always post an -openGLView:didUpdateToTime: if a previous one isn't still ongoing. This is the hook upon which the substantial processing occurs.
if(!OSAtomicTestAndSet(processingMask, &_updateIsOngoing))
{
CVTimeStamp time = *now;
BOOL didSkip = _hasSkipped;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_async(_serialDispatchQueue, ^{
[self.delegate openGLView:self didUpdateToTime:time didSkipPreviousUpdate:didSkip frequency:frequency];
[self drawViewOnlyIfDirty:YES];
OSAtomicTestAndClear(processingMask, &_updateIsOngoing);
});
_hasSkipped = NO;
} else _hasSkipped = YES;
// Draw the display only if a previous draw is not still ongoing. -drawViewOnlyIfDirty: is guaranteed
// to be safe to call concurrently with -openGLView:updateToTime: so there's no need to worry about
// the above interrupting the below or vice versa.
if(!OSAtomicTestAndSet(drawingMask, &_updateIsOngoing) && _hasSkipped)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self drawViewOnlyIfDirty:YES];
OSAtomicTestAndClear(drawingMask, &_updateIsOngoing);
});
}
else
{
_hasSkipped = YES;
}
// Draw the display now regardless of other activity.
[self drawViewOnlyIfDirty:YES];
}
- (void)invalidate

View File

@ -10,6 +10,11 @@
@interface AudioQueue : NSObject
- (instancetype)initWithSamplingRate:(Float64)samplingRate;
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples;
@property (nonatomic, readonly) Float64 samplingRate;
+ (Float64)preferredSamplingRate;
@end

View File

@ -11,7 +11,7 @@
#define AudioQueueNumAudioBuffers 4
#define AudioQueueStreamLength 1024
#define AudioQueueBufferLength 256
#define AudioQueueBufferLength 512
enum {
AudioQueueCanProceed,
@ -85,20 +85,21 @@ static void audioOutputCallback(
[(__bridge AudioQueue *)inUserData audioQueue:inAQ didCallbackWithBuffer:inBuffer];
}
- (instancetype)init
- (instancetype)initWithSamplingRate:(Float64)samplingRate
{
self = [super init];
if(self)
{
_writeLock = [[NSConditionLock alloc] initWithCondition:AudioQueueCanProceed];
_samplingRate = samplingRate;
/*
Describe a mono, 16bit, 44.1Khz audio format
*/
AudioStreamBasicDescription outputDescription;
outputDescription.mSampleRate = 44100;
outputDescription.mSampleRate = samplingRate;
outputDescription.mFormatID = kAudioFormatLinearPCM;
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
@ -113,13 +114,13 @@ static void audioOutputCallback(
// create an audio output queue along those lines
if(!AudioQueueNewOutput(
&outputDescription,
audioOutputCallback,
(__bridge void *)(self),
NULL,
kCFRunLoopCommonModes,
0,
&_audioQueue))
&outputDescription,
audioOutputCallback,
(__bridge void *)(self),
NULL,
kCFRunLoopCommonModes,
0,
&_audioQueue))
{
UInt32 bufferBytes = AudioQueueBufferLength * sizeof(int16_t);
@ -139,6 +140,11 @@ static void audioOutputCallback(
return self;
}
- (instancetype)init
{
return [self initWithSamplingRate:[[self class] preferredSamplingRate]];
}
- (void)dealloc
{
[_writeLock lock];
@ -190,4 +196,28 @@ static void audioOutputCallback(
return ((_audioStreamWritePosition - _audioStreamReadPosition) < (AudioQueueStreamLength - AudioQueueBufferLength)) ? AudioQueueCanProceed : AudioQueueWait;
}
+ (AudioDeviceID)defaultOutputDevice
{
AudioObjectPropertyAddress address;
address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
address.mScope = kAudioObjectPropertyScopeGlobal;
address.mElement = kAudioObjectPropertyElementMaster;
AudioDeviceID deviceID;
UInt32 size = sizeof(AudioDeviceID);
return AudioHardwareServiceGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &deviceID) ? 0 : deviceID;
}
+ (Float64)preferredSamplingRate
{
AudioObjectPropertyAddress address;
address.mSelector = kAudioDevicePropertyNominalSampleRate;
address.mScope = kAudioObjectPropertyScopeGlobal;
address.mElement = kAudioObjectPropertyElementMaster;
Float64 samplingRate;
UInt32 size = sizeof(Float64);
return AudioHardwareServiceGetPropertyData([self defaultOutputDevice], &address, 0, NULL, &size, &samplingRate) ? 0.0 : samplingRate;
}
@end

View File

@ -15,6 +15,4 @@
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput;
- (void)setResetLineEnabled:(BOOL)enabled;
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
@end

View File

@ -49,16 +49,6 @@ struct CRTDelegate: public Outputs::CRT::Delegate {
}
}
- (void)runForNumberOfCycles:(int)numberOfCycles {
@synchronized(self) {
_atari2600.run_for_cycles(numberOfCycles);
}
}
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {
_atari2600.get_crt()->draw_frame((unsigned int)pixelSize.width, (unsigned int)pixelSize.height, onlyIfDirty ? true : false);
}
- (void)setROM:(NSData *)rom {
@synchronized(self) {
_atari2600.set_rom(rom.length, (const uint8_t *)rom.bytes);
@ -79,16 +69,14 @@ struct CRTDelegate: public Outputs::CRT::Delegate {
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) {
_atari2600.setup_output(aspectRatio);
[super setupOutputWithAspectRatio:aspectRatio];
_atari2600.get_crt()->set_delegate(&_crtDelegate);
_crtDelegate.atari2600 = self;
}
}
- (void)closeOutput {
@synchronized(self) {
_atari2600.close_output();
}
- (CRTMachine::Machine * const)machine {
return &_atari2600;
}
@end

View File

@ -19,8 +19,6 @@
- (void)setKey:(uint16_t)key isPressed:(BOOL)isPressed;
- (void)clearAllKeys;
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
@property (nonatomic, assign) BOOL useFastLoadingHack;
@property (nonatomic, assign) BOOL useTelevisionOutput;

View File

@ -16,11 +16,8 @@
Electron::Machine _electron;
}
- (void)runForNumberOfCycles:(int)numberOfCycles {
@synchronized(self) {
_electron.run_for_cycles(numberOfCycles);
_electron.update_output();
}
- (CRTMachine::Machine * const)machine {
return &_electron;
}
- (void)setOSROM:(nonnull NSData *)rom {
@ -41,10 +38,6 @@
}
}
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {
_electron.get_crt()->draw_frame((unsigned int)pixelSize.width, (unsigned int)pixelSize.height, onlyIfDirty ? true : false);
}
- (BOOL)openUEFAtURL:(NSURL *)URL {
@synchronized(self) {
try {
@ -57,24 +50,13 @@
}
}
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate {
- (void)clearAllKeys {
@synchronized(self) {
_electron.get_speaker()->set_output_rate(sampleRate, 256);
_electron.get_speaker()->set_delegate(delegate);
return YES;
_electron.clear_all_keys();
}
}
- (void)clearAllKeys {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
@synchronized(self) {
_electron.clear_all_keys();
}
// });
}
- (void)setKey:(uint16_t)key isPressed:(BOOL)isPressed {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
@synchronized(self) {
switch(key)
{
@ -152,16 +134,13 @@
break;
}
}
// });
}
- (void)setUseFastLoadingHack:(BOOL)useFastLoadingHack {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
@synchronized(self) {
_useFastLoadingHack = useFastLoadingHack;
_electron.set_use_fast_tape_hack(useFastLoadingHack ? true : false);
}
// });
@synchronized(self) {
_useFastLoadingHack = useFastLoadingHack;
_electron.set_use_fast_tape_hack(useFastLoadingHack ? true : false);
}
}
- (void)setUseTelevisionOutput:(BOOL)useTelevisionOutput {
@ -171,16 +150,4 @@
}
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) {
_electron.setup_output(aspectRatio);
}
}
- (void)closeOutput {
@synchronized(self) {
_electron.close_output();
}
}
@end

View File

@ -7,16 +7,15 @@
//
#import "CSMachine.h"
#include "CRT.hpp"
#include "Speaker.hpp"
#include "CRTMachine.hpp"
@interface CSMachine (Subclassing)
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate;
- (void)speaker:(Outputs::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)performAsync:(dispatch_block_t)action;
- (void)performSync:(dispatch_block_t)action;
- (CRTMachine::Machine * const)machine;
- (void)setupOutputWithAspectRatio:(float)aspectRatio;
- (void)closeOutput;
@end

View File

@ -13,7 +13,12 @@
@interface CSMachine : NSObject
- (void)runForNumberOfCycles:(int)numberOfCycles;
- (int)idealSamplingRateFromRange:(NSRange)range;
- (void)setAudioSamplingRate:(int)samplingRate;
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio;
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
@property (nonatomic, weak) AudioQueue *audioQueue;
@property (nonatomic, readonly) CSOpenGLView *view;

View File

@ -9,6 +9,10 @@
#import "CSMachine.h"
#import "CSMachine+Subclassing.h"
@interface CSMachine()
- (void)speaker:(Outputs::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
@end
struct SpeakerDelegate: public Outputs::Speaker::Delegate {
__weak CSMachine *machine;
void speaker_did_complete_samples(Outputs::Speaker *speaker, const int16_t *buffer, int buffer_size) {
@ -30,8 +34,6 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
if(self) {
_serialDispatchQueue = dispatch_queue_create("Machine queue", DISPATCH_QUEUE_SERIAL);
_speakerDelegate.machine = self;
[self setSpeakerDelegate:&_speakerDelegate sampleRate:44100];
}
return self;
@ -39,15 +41,48 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
- (void)dealloc {
[_view performWithGLContext:^{
[self closeOutput];
@synchronized(self) {
self.machine->close_output();
}
}];
}
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate {
return NO;
- (int)idealSamplingRateFromRange:(NSRange)range {
@synchronized(self) {
Outputs::Speaker *speaker = self.machine->get_speaker();
if(speaker)
{
return speaker->get_ideal_clock_rate_in_range((int)range.location, (int)(range.location + range.length));
}
return (int)range.location;
}
}
- (void)runForNumberOfCycles:(int)numberOfCycles {}
- (void)setAudioSamplingRate:(int)samplingRate {
@synchronized(self) {
_speakerDelegate.machine = self;
[self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate];
}
}
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate {
@synchronized(self) {
Outputs::Speaker *speaker = self.machine->get_speaker();
if(speaker)
{
speaker->set_output_rate(sampleRate, 512);
speaker->set_delegate(delegate);
return YES;
}
return NO;
}
}
- (void)runForNumberOfCycles:(int)numberOfCycles {
@synchronized(self) {
self.machine->run_for_cycles(numberOfCycles);
}
}
- (void)performSync:(dispatch_block_t)action {
dispatch_sync(_serialDispatchQueue, action);
@ -57,10 +92,6 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
dispatch_async(_serialDispatchQueue, action);
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {}
- (void)closeOutput {}
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
_view = view;
[view performWithGLContext:^{
@ -68,4 +99,13 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
}];
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
self.machine->setup_output(aspectRatio);
}
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {
self.machine->get_crt()->draw_frame((unsigned int)pixelSize.width, (unsigned int)pixelSize.height, onlyIfDirty ? true : false);
}
@end

View File

@ -108,6 +108,7 @@ namespace {
OpenGLOutputBuilder::OpenGLOutputBuilder(unsigned int buffer_depth) :
_output_mutex(new std::mutex),
_draw_mutex(new std::mutex),
_visible_area(Rect(0, 0, 1, 1)),
_composite_src_output_y(0),
_cleared_composite_output_y(0),
@ -172,8 +173,8 @@ OpenGLOutputBuilder::~OpenGLOutputBuilder()
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
{
// lock down any further work on the current frame
_output_mutex->lock();
// lock down any other draw_frames
_draw_mutex->lock();
// establish essentials
if(!output_shader_program)
@ -195,21 +196,6 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
glDeleteSync(_fence);
}
// release the mapping, giving up on trying to draw if data has been lost
GLsizei submitted_output_data = submitArrayData(output_array_buffer, _output_buffer_data.get(), &_output_buffer_data_pointer);
// bind and flush the source array buffer
GLsizei submitted_source_data = submitArrayData(source_array_buffer, _source_buffer_data.get(), &_source_buffer_data_pointer);
// determine how many lines are newly reclaimed; they'll need to be cleared
Range clearing_zones[2];
// the clearing zones for the composite output Y are calculated with a fixed offset of '1' which has the effect of clearing
// one ahead of the expected drawing area this frame; that's because the current _composite_src_output_y may or may not have been
// written to during the last update, so we want it to have been cleared during the last update.
int number_of_clearing_zones = getCircularRanges(&_cleared_composite_output_y, &_composite_src_output_y, IntermediateBufferHeight, 1, 1, clearing_zones);
uint16_t completed_texture_y = _buffer_builder->get_and_finalise_current_line();
// make sure there's a target to draw to
if(!framebuffer || framebuffer->get_height() != output_height || framebuffer->get_width() != output_width)
{
@ -228,6 +214,24 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
framebuffer = std::move(new_framebuffer);
}
// lock out the machine emulation until data is copied
_output_mutex->lock();
// release the mapping, giving up on trying to draw if data has been lost
GLsizei submitted_output_data = submitArrayData(output_array_buffer, _output_buffer_data.get(), &_output_buffer_data_pointer);
// bind and flush the source array buffer
GLsizei submitted_source_data = submitArrayData(source_array_buffer, _source_buffer_data.get(), &_source_buffer_data_pointer);
// determine how many lines are newly reclaimed; they'll need to be cleared
Range clearing_zones[2];
// the clearing zones for the composite output Y are calculated with a fixed offset of '1' which has the effect of clearing
// one ahead of the expected drawing area this frame; that's because the current _composite_src_output_y may or may not have been
// written to during the last update, so we want it to have been cleared during the last update.
int number_of_clearing_zones = getCircularRanges(&_cleared_composite_output_y, &_composite_src_output_y, IntermediateBufferHeight, 1, 1, clearing_zones);
uint16_t completed_texture_y = _buffer_builder->get_and_finalise_current_line();
// upload new source pixels
if(completed_texture_y)
{
@ -239,6 +243,9 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
_buffer_builder->get_image_pointer());
}
// data having been grabbed, allow the machine to continue
_output_mutex->unlock();
struct RenderStage {
OpenGL::TextureTarget *const target;
OpenGL::Shader *const shader;
@ -330,7 +337,7 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
framebuffer->draw((float)output_width / (float)output_height);
_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
_output_mutex->unlock();
_draw_mutex->unlock();
}
void OpenGLOutputBuilder::reset_all_OpenGL_state()

View File

@ -58,6 +58,7 @@ class OpenGLOutputBuilder {
// the run and input data buffers
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
std::unique_ptr<std::mutex> _output_mutex;
std::unique_ptr<std::mutex> _draw_mutex;
// transient buffers indicating composite data not yet decoded
GLsizei _composite_src_output_y, _cleared_composite_output_y;

View File

@ -269,7 +269,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shade
"), vec3(1.0))"
");"
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 4.0, 4.0);"
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 2.0, 2.0);"
"fragColour = lumaChromaToRGB * lumaChromaColourInRange;"
"}", false, false);
}

View File

@ -78,7 +78,7 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
"void main(void)"
"{"
"fragColour = vec4(%s, 0.5*cos(lateralVarying));"
"fragColour = vec4(%s, 0.5);"//*cos(lateralVarying)
"}",
sampler_type, fragment_methods, colour_expression);

View File

@ -24,6 +24,18 @@ class Speaker {
virtual void speaker_did_complete_samples(Speaker *speaker, const int16_t *buffer, int buffer_size) = 0;
};
int get_ideal_clock_rate_in_range(int minimum, int maximum)
{
// return exactly the input rate if possible
if(_input_cycles_per_second >= minimum && _input_cycles_per_second <= maximum) return _input_cycles_per_second;
// if the input rate is lower, return the minimum
if(_input_cycles_per_second < minimum) return minimum;
// otherwise, return the maximum
return maximum;
}
void set_output_rate(int cycles_per_second, int buffer_size)
{
_output_cycles_per_second = cycles_per_second;
@ -76,20 +88,16 @@ template <class T> class Filter: public Speaker {
{
if(_coefficients_are_dirty) update_filter_coefficients();
// TODO: what if output rate is greater than input rate?
// fill up as much of the input buffer as possible
while(input_cycles)
// if input and output rates exactly match, just accumulate results and pass on
if(_input_cycles_per_second == _output_cycles_per_second)
{
unsigned int cycles_to_read = (unsigned int)std::min((int)input_cycles, _number_of_taps - _input_buffer_depth);
static_cast<T *>(this)->get_samples(cycles_to_read, &_input_buffer.get()[_input_buffer_depth]);
input_cycles -= cycles_to_read;
_input_buffer_depth += cycles_to_read;
if(_input_buffer_depth == _number_of_taps)
while(input_cycles)
{
_buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get());
_buffer_in_progress_pointer++;
unsigned int cycles_to_read = (unsigned int)(_buffer_size - _buffer_in_progress_pointer);
if(cycles_to_read > input_cycles) cycles_to_read = input_cycles;
static_cast<T *>(this)->get_samples(cycles_to_read, &_buffer_in_progress.get()[_buffer_in_progress_pointer]);
_buffer_in_progress_pointer += cycles_to_read;
// announce to delegate if full
if(_buffer_in_progress_pointer == _buffer_size)
@ -101,24 +109,60 @@ template <class T> class Filter: public Speaker {
}
}
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
uint64_t steps = _stepper->step();
if(steps < _number_of_taps)
input_cycles -= cycles_to_read;
}
return;
}
// if the output rate is less than the input rate, use the filter
if(_input_cycles_per_second > _output_cycles_per_second)
{
while(input_cycles)
{
unsigned int cycles_to_read = (unsigned int)std::min((int)input_cycles, _number_of_taps - _input_buffer_depth);
static_cast<T *>(this)->get_samples(cycles_to_read, &_input_buffer.get()[_input_buffer_depth]);
input_cycles -= cycles_to_read;
_input_buffer_depth += cycles_to_read;
if(_input_buffer_depth == _number_of_taps)
{
int16_t *input_buffer = _input_buffer.get();
memmove(input_buffer, &input_buffer[steps], sizeof(int16_t) * ((size_t)_number_of_taps - (size_t)steps));
_input_buffer_depth -= steps;
}
else
{
if(steps > _number_of_taps)
static_cast<T *>(this)->skip_samples((unsigned int)steps - (unsigned int)_number_of_taps);
_input_buffer_depth = 0;
_buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get());
_buffer_in_progress_pointer++;
// announce to delegate if full
if(_buffer_in_progress_pointer == _buffer_size)
{
_buffer_in_progress_pointer = 0;
if(_delegate)
{
_delegate->speaker_did_complete_samples(this, _buffer_in_progress.get(), _buffer_size);
}
}
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
uint64_t steps = _stepper->step();
if(steps < _number_of_taps)
{
int16_t *input_buffer = _input_buffer.get();
memmove(input_buffer, &input_buffer[steps], sizeof(int16_t) * ((size_t)_number_of_taps - (size_t)steps));
_input_buffer_depth -= steps;
}
else
{
if(steps > _number_of_taps)
static_cast<T *>(this)->skip_samples((unsigned int)steps - (unsigned int)_number_of_taps);
_input_buffer_depth = 0;
}
}
}
return;
}
// TODO: input rate is less than output rate
}
private:

View File

@ -67,9 +67,12 @@ extern const uint8_t JamOpcode;
@abstact An abstract base class for emulation of a 6502 processor via the curiously recurring template pattern/f-bounded polymorphism.
@discussion Subclasses should implement @c perform_bus_operation(BusOperation operation, uint16_t address, uint8_t *value) in
order to provde the bus on which the 6502 operates. Additional functionality can be provided by the host machine by providing
a jam handler and inserting jam opcodes where appropriate; that will cause call outs when the program counter reaches those
addresses. @c return_from_subroutine can be used to exit from a jammed state.
order to provide the bus on which the 6502 operates and @c synchronise(), which is called upon completion of a continuous run
of cycles to allow a subclass to bring any on-demand activities up to date.
Additional functionality can be provided by the host machine by providing a jam handler and inserting jam opcodes where appropriate;
that will cause call outs when the program counter reaches those addresses. @c return_from_subroutine can be used to exit from a
jammed state.
*/
template <class T> class Processor {
public:
@ -597,6 +600,7 @@ template <class T> class Processor {
case CycleFetchOperation: {
_lastOperationPC = _pc;
// printf("%04x x:%02x\n", _pc.full, _x);
_pc.full++;
read_op(_operation, _lastOperationPC.full);
@ -1040,15 +1044,17 @@ template <class T> class Processor {
_ready_is_active = true;
}
}
_cycles_left_to_run = number_of_cycles;
_scheduleProgramsReadPointer = scheduleProgramsReadPointer;
_scheduleProgramProgramCounter = scheduleProgramProgramCounter;
_nextAddress = nextAddress;
_nextBusOperation = nextBusOperation;
_busAddress = busAddress;
_busValue = busValue;
}
_cycles_left_to_run = number_of_cycles;
_scheduleProgramsReadPointer = scheduleProgramsReadPointer;
_scheduleProgramProgramCounter = scheduleProgramProgramCounter;
_nextAddress = nextAddress;
_nextBusOperation = nextBusOperation;
_busAddress = busAddress;
_busValue = busValue;
static_cast<T *>(this)->synchronise();
}
/*!