1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-24 12:29:06 +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> #include <stdio.h>
using namespace Atari2600; using namespace Atari2600;
static const int horizontalTimerReload = 227; namespace {
static const unsigned int horizontalTimerPeriod = 228;
}
Machine::Machine() : Machine::Machine() :
_horizontalTimer(0), _horizontalTimer(0),
@ -19,12 +21,39 @@ Machine::Machine() :
_lastOutputState(OutputState::Sync), _lastOutputState(OutputState::Sync),
_piaTimerStatus(0xff), _piaTimerStatus(0xff),
_rom(nullptr), _rom(nullptr),
_hMoveWillCount(false),
_piaDataValue{0xff, 0xff}, _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)); memset(_collisions, 0xff, sizeof(_collisions));
set_reset_line(true); 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) 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);" "return (float(y) / 14.0) * (1.0 - amplitude) + step(1, iPhase) * amplitude * cos(phase + phaseOffset);"
"}"); "}");
_crt->set_output_device(Outputs::CRT::Television); _crt->set_output_device(Outputs::CRT::Television);
_speaker.set_input_rate(1194720 / 38);
} }
void Machine::switch_region() 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);" "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); _crt->set_new_timing(228, 312, Outputs::CRT::ColourSpace::YUV, 228, 1);
// _speaker.set_input_rate(2 * 312 * 50);
} }
void Machine::close_output() void Machine::close_output()
@ -75,96 +108,133 @@ Machine::~Machine()
close_output(); 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 unsigned int upcomingPointerPlus4 = (_upcomingEventsPointer + 4)%number_of_upcoming_events;
uint8_t playfieldPixel = _playfield[offset >> 2];
uint8_t playfieldColour = ((_playfieldControl&6) == 2) ? _playerColour[offset / 80] : _playfieldColour;
// get player and missile proposed pixels _objectCounterPointer = (_objectCounterPointer + 1)%number_of_recorded_counters;
uint8_t playerPixels[2] = {0, 0}, missilePixels[2] = {0, 0}; ObjectCounter *oneClockAgo = _objectCounter[(_objectCounterPointer - 1 + number_of_recorded_counters)%number_of_recorded_counters];
for(int c = 0; c < 2; c++) 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; unsigned int offset = 4 + _horizontalTimer - (horizontalTimerPeriod - 160);
if(_playerGraphics[c]) { _upcomingEvents[upcomingPointerPlus4].updates |= Event::Action::Playfield;
// figure out player colour _upcomingEvents[upcomingPointerPlus4].playfieldPixel = _playfield[(offset >> 2)%40];
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;
}
} }
// 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; uint8_t ballPixel = 0;
if(_ballGraphicsEnable&2) { if(now[4].pixel < _ballSize) {
int ballIndex = _objectCounter[4] - 4; ballPixel = _ballGraphicsEnable[_ballGraphicsSelector];
int ballSize = 1 << ((_playfieldControl >> 4)&3); }
ballPixel = (ballIndex >= 0 && ballIndex < ballSize) ? 1 : 0;
// 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 // accumulate collisions
if(playerPixels[0] | playerPixels[1]) { int pixel_mask = playerPixels[0] | (playerPixels[1] << 1) | (missilePixels[0] << 2) | (missilePixels[1] << 3) | (ballPixel << 4) | (_playfieldOutput << 5);
_collisions[0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6); _collisions[0] |= _reportedCollisions[pixel_mask][0];
_collisions[1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6); _collisions[1] |= _reportedCollisions[pixel_mask][1];
_collisions[2] |= _reportedCollisions[pixel_mask][2];
_collisions[2] |= ((playfieldPixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6); _collisions[3] |= _reportedCollisions[pixel_mask][3];
_collisions[3] |= ((playfieldPixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6); _collisions[4] |= _reportedCollisions[pixel_mask][4];
_collisions[5] |= _reportedCollisions[pixel_mask][5];
_collisions[7] |= ((playerPixels[0] & playerPixels[1]) << 7); _collisions[6] |= _reportedCollisions[pixel_mask][6];
} _collisions[7] |= _reportedCollisions[pixel_mask][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);
// apply appropriate priority to pick a colour // apply appropriate priority to pick a colour
playfieldPixel |= ballPixel; uint8_t playfieldPixel = _playfieldOutput | ballPixel;
uint8_t outputColour = playfieldPixel ? playfieldColour : _backgroundColour; uint8_t outputColour = playfieldPixel ? playfieldColour : _backgroundColour;
if(!(_playfieldControl&0x04) || !playfieldPixel) { 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]; if(playerPixels[0] || missilePixels[0]) outputColour = _playerColour[0];
} }
// store colour // return colour
// static int lc; return outputColour;
// if(_vSyncEnabled) lc = 0; else lc += (offset == 159) ? 1 : 0;
// *pixel = (uint8_t)(((offset / 10) << 4) | (((lc >> 4)&7) << 1));
*pixel = outputColour;
} }
// in imputing the knowledge that all we're dealing with is the rollover from 159 to 0, void Machine::setup_reported_collisions()
// this is faster than the straightforward +1)%160 per profiling {
#define increment_object_counter(c) _objectCounter[c] = (_objectCounter[c]+1)&~((158-_objectCounter[c]) >> 8) 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) 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--) 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 // clear any ongoing moves
if(!(_horizontalTimer&3)) { if(_hMoveFlags)
{
for(int c = 0; c < number_of_upcoming_events; c++)
{
_upcomingEvents[c].updates &= ~(Event::Action::HMoveCompare | Event::Action::HMoveDecrement);
}
}
if(_hMoveFlags) { // schedule new moves
const uint8_t counterValue = _hMoveCounter ^ 0x7; _hMoveFlags = 0x1f;
for(int c = 0; c < 5; c++) { _hMoveCounter = 15;
if(counterValue == (_objectMotion[c] >> 4)) _hMoveFlags &= ~(1 << c);
if(_hMoveFlags&(1 << c)) increment_object_counter(c); // 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) { if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveDecrement)
_hMoveIsCounting = !!_hMoveCounter; {
_hMoveCounter = (_hMoveCounter-1)&0xf; 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 // update pixel timers
// 4 sync if(primary_state == OutputState::Pixel) update_timers(~0);
// 9 'blank'; colour burst after 4
// 40 pixels
// it'll be about 43 cycles from start of hsync to start of visible frame, so... // update the background chain
// guesses, until I can find information: 26 cycles blank, 16 sync, 40 blank, 160 pixels if(_horizontalTimer >= 64 && _horizontalTimer <= 160+64 && !(_horizontalTimer&3))
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) { {
if(_vBlankEnabled) { _playfieldOutput = _nextPlayfieldOutput;
state = OutputState::Blank; _nextPlayfieldOutput = _playfield[(_horizontalTimer - 64) >> 2];
} else {
state = OutputState::Pixel;
}
} }
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) { 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++; _lastOutputStateDuration++;
if(state != _lastOutputState) { if(effective_state != _lastOutputState) {
switch(_lastOutputState) { switch(_lastOutputState) {
case OutputState::Blank: _crt->output_blank(_lastOutputStateDuration); break; case OutputState::Blank: _crt->output_blank(_lastOutputStateDuration); break;
case OutputState::Sync: _crt->output_sync(_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; case OutputState::Pixel: _crt->output_data(_lastOutputStateDuration, 1); break;
} }
_lastOutputStateDuration = 0; _lastOutputStateDuration = 0;
_lastOutputState = state; _lastOutputState = effective_state;
if(state == OutputState::Pixel) { if(effective_state == OutputState::Pixel) {
_outputBuffer = _crt->allocate_write_area(160); _outputBuffer = _crt->allocate_write_area(160);
} else { } else {
_outputBuffer = nullptr; _outputBuffer = nullptr;
} }
} }
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) { // decide on a pixel colour if that's what's happening
uint8_t throwaway_pixel; if(effective_state == OutputState::Pixel)
get_output_pixel(_outputBuffer ? &_outputBuffer[_lastOutputStateDuration] : &throwaway_pixel, 159 - _horizontalTimer); {
uint8_t colour = get_output_pixel();
// increment all graphics counters if(_outputBuffer)
increment_object_counter(0); {
increment_object_counter(1); *_outputBuffer = colour;
increment_object_counter(2); _outputBuffer++;
increment_object_counter(3); }
increment_object_counter(4);
} }
// assumption here: signed shifts right; otherwise it's just // advance horizontal timer, perform reset actions if desired
// an attempt to avoid both the % operator and a conditional _horizontalTimer = (_horizontalTimer + 1) % horizontalTimerPeriod;
_horizontalTimer--;
const int32_t sign_extension = _horizontalTimer >> 31;
_horizontalTimer = (_horizontalTimer&~sign_extension) | (sign_extension&horizontalTimerReload);
if(!_horizontalTimer) 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); set_reset_line(false);
uint8_t returnValue = 0xff; uint8_t returnValue = 0xff;
unsigned int cycles_run_for = 1; unsigned int cycles_run_for = 3;
const int32_t ready_line_disable_time = 227;//horizontalTimerReload;
// 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) { if(operation == CPU6502::BusOperation::Ready) {
unsigned int distance_to_end_of_ready = (_horizontalTimer - ready_line_disable_time + horizontalTimerReload + 1)%(horizontalTimerReload + 1); unsigned int distance_to_end_of_ready = horizontalTimerPeriod - _horizontalTimer;
cycles_run_for = distance_to_end_of_ready / 3; cycles_run_for = distance_to_end_of_ready;
output_pixels(distance_to_end_of_ready);
} else {
output_pixels(3);
} }
if(_hMoveWillCount) { output_pixels(cycles_run_for);
_hMoveCounter = 0x0f; _cycles_since_speaker_update += cycles_run_for;
_hMoveFlags = 0x1f;
_hMoveIsCounting = true;
_hMoveWillCount = false;
}
if(_horizontalTimer == ready_line_disable_time)
set_ready_line(false);
if(operation != CPU6502::BusOperation::Ready) { 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 0x01: _vBlankEnabled = !!(*value & 0x02); break;
case 0x02: case 0x02:
set_ready_line(true); if(_horizontalTimer) set_ready_line(true);
break; break;
case 0x03: case 0x03:
_horizontalTimer = 0; // Reset is delayed by four cycles.
_horizontalTimer = horizontalTimerPeriod - 4;
// TODO: audio will now be out of synchronisation — fix
break; break;
case 0x04: 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 0x06:
case 0x07: _playerColour[decodedAddress - 0x06] = *value; break; case 0x07: _playerColour[decodedAddress - 0x06] = *value; break;
@ -395,6 +527,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x0a: { case 0x0a: {
uint8_t old_playfield_control = _playfieldControl; uint8_t old_playfield_control = _playfieldControl;
_playfieldControl = *value; _playfieldControl = *value;
_ballSize = 1 << ((_playfieldControl >> 4)&3);
// did the mirroring bit change? // did the mirroring bit change?
if((_playfieldControl^old_playfield_control)&1) { if((_playfieldControl^old_playfield_control)&1) {
@ -406,7 +539,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
} }
} break; } break;
case 0x0b: case 0x0b:
case 0x0c: _playerReflection[decodedAddress - 0x0b] = *value; break; case 0x0c: _playerReflectionMask[decodedAddress - 0x0b] = (*value)&8 ? 0 : 7; break;
case 0x0d: case 0x0d:
_playfield[0] = ((*value) >> 4)&1; _playfield[0] = ((*value) >> 4)&1;
@ -454,23 +587,40 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
break; break;
case 0x10: case 0x11: case 0x12: case 0x13: 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: case 0x1c:
_ballGraphicsEnable = _ballGraphicsEnableLatch; _ballGraphicsEnable[1] = _ballGraphicsEnable[0];
case 0x1b: { case 0x1b: {
int index = decodedAddress - 0x1b; int index = decodedAddress - 0x1b;
_playerGraphicsLatch[index] = *value; _playerGraphics[0][index] = *value;
if(!(_playerGraphicsLatchEnable[index]&1)) _playerGraphics[1][index^1] = _playerGraphics[0][index^1];
_playerGraphics[index] = _playerGraphicsLatch[index];
_playerGraphics[index^1] = _playerGraphicsLatch[index^1];
} break; } break;
case 0x1d: _missileGraphicsEnable[0] = *value; break; case 0x1d:
case 0x1e: _missileGraphicsEnable[1] = *value; break; case 0x1e:
_missileGraphicsEnable[decodedAddress - 0x1d] = ((*value) >> 1)&1;
// printf("e:%02x <- %c\n", decodedAddress - 0x1d, ((*value)&1) ? 'E' : '-');
break;
case 0x1f: case 0x1f:
_ballGraphicsEnableLatch = *value; _ballGraphicsEnable[0] = ((*value) >> 1)&1;
if(!(_ballGraphicsEnableDelay&1))
_ballGraphicsEnable = _ballGraphicsEnableLatch;
break; break;
case 0x20: case 0x20:
@ -481,21 +631,42 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
_objectMotion[decodedAddress - 0x20] = *value; _objectMotion[decodedAddress - 0x20] = *value;
break; break;
case 0x25: _playerGraphicsLatchEnable[0] = *value; break; case 0x25: _playerGraphicsSelector[0] = (*value)&1; break;
case 0x26: _playerGraphicsLatchEnable[1] = *value; break; case 0x26: _playerGraphicsSelector[1] = (*value)&1; break;
case 0x27: _ballGraphicsEnableDelay = *value; break; case 0x27: _ballGraphicsSelector = (*value)&1; break;
case 0x28: case 0x28:
case 0x29: 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 // TODO: this should properly mean setting a flag and propagating later, I think?
_missileGraphicsReset[decodedAddress - 0x28] = *value; 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; break;
case 0x2a: case 0x2a: {
_vBlankExtend = true; // justification for +5: "we need to wait at least 71 [clocks] before the HMOVE operation is complete";
_hMoveWillCount = true; // which will take 16*4 + 2 = 66 cycles from the first compare, implying the first compare must be
break; // 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: case 0x2b:
_objectMotion[0] = _objectMotion[0] =
_objectMotion[1] = _objectMotion[1] =
@ -525,8 +696,10 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x01: case 0x01:
case 0x03: case 0x03:
// TODO: port DDR // TODO: port DDR
printf("!!!DDR!!!");
break; break;
case 0x04: case 0x04:
case 0x06:
returnValue &= _piaTimerValue >> _piaTimerShift; returnValue &= _piaTimerValue >> _piaTimerShift;
if(_writtenPiaTimerShift != _piaTimerShift) { if(_writtenPiaTimerShift != _piaTimerShift) {
@ -535,8 +708,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
} }
break; break;
case 0x05: case 0x05:
case 0x07:
returnValue &= _piaTimerStatus; returnValue &= _piaTimerStatus;
_piaTimerStatus &= ~0x40; _piaTimerStatus &= ~0x80;
break; break;
} }
} else { } else {
@ -546,9 +720,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x05: case 0x05:
case 0x06: case 0x06:
case 0x07: case 0x07:
_writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07); _writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07); // i.e. 0, 3, 6, 10
_piaTimerValue = (unsigned int)(*value << _piaTimerShift); _piaTimerValue = (unsigned int)(*value) << _piaTimerShift;
_piaTimerStatus &= ~0xc0; _piaTimerStatus &= ~0x40;
break; break;
} }
} }
@ -559,15 +733,26 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
} }
} }
if(_piaTimerValue >= cycles_run_for) { if(_piaTimerValue >= cycles_run_for / 3) {
_piaTimerValue -= cycles_run_for; _piaTimerValue -= cycles_run_for / 3;
} else { } else {
_piaTimerValue += 0xff - cycles_run_for; _piaTimerValue = 0x100 + ((_piaTimerValue - (cycles_run_for / 3)) >> _piaTimerShift);
_piaTimerShift = 0; _piaTimerShift = 0;
_piaTimerStatus |= 0xc0; _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) 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[2] = &_rom[2048 & romMask];
_romPages[3] = &_rom[3072 & 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 #define Atari2600_cpp
#include "../../Processors/6502/CPU6502.hpp" #include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp" #include "../CRTMachine.hpp"
#include <stdint.h> #include <stdint.h>
#include "Atari2600Inputs.h" #include "Atari2600Inputs.h"
namespace Atari2600 { 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: public:
Machine(); Machine();
~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 set_rom(size_t length, const uint8_t *data);
void switch_region(); void switch_region();
void set_digital_input(Atari2600DigitalInput input, bool state); void set_digital_input(Atari2600DigitalInput input, bool state);
Outputs::CRT::CRT *get_crt() { return _crt; } // to satisfy CPU6502::Processor
void setup_output(float aspect_ratio); unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void close_output(); 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: private:
uint8_t *_rom, *_romPages[4], _ram[128]; uint8_t *_rom, *_romPages[4], _ram[128];
@ -46,32 +82,81 @@ class Machine: public CPU6502::Processor<Machine> {
uint8_t _playfieldControl; uint8_t _playfieldControl;
uint8_t _playfieldColour; uint8_t _playfieldColour;
uint8_t _backgroundColour; 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 // player registers
uint8_t _playerColour[2]; uint8_t _playerColour[2];
uint8_t _playerReflection[2]; uint8_t _playerReflectionMask[2];
uint8_t _playerGraphicsLatch[2], _playerGraphics[2]; uint8_t _playerGraphics[2][2];
uint8_t _playerGraphicsLatchEnable[2]; uint8_t _playerGraphicsSelector[2];
bool _playerStart[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 // player + missile registers
uint8_t _playerAndMissileSize[2]; uint8_t _playerAndMissileSize[2];
// missile registers // missile registers
uint8_t _missileGraphicsEnable[2], _missileGraphicsReset[2]; uint8_t _missileGraphicsEnable[2];
bool _missileGraphicsReset[2];
// ball registers // ball registers
uint8_t _ballGraphicsEnable, _ballGraphicsEnableLatch; uint8_t _ballGraphicsEnable[2];
uint8_t _ballGraphicsEnableDelay; uint8_t _ballGraphicsSelector;
// graphics output // graphics output
int32_t _horizontalTimer; unsigned int _horizontalTimer;
bool _vSyncEnabled, _vBlankEnabled; bool _vSyncEnabled, _vBlankEnabled;
bool _vBlankExtend;
// horizontal motion control
uint8_t _hMoveCounter; uint8_t _hMoveCounter;
bool _hMoveIsCounting, _hMoveWillCount;
uint8_t _objectCounter[5], _objectMotion[5];
uint8_t _hMoveFlags; uint8_t _hMoveFlags;
// joystick state // joystick state
@ -82,21 +167,28 @@ class Machine: public CPU6502::Processor<Machine> {
// collisions // collisions
uint8_t _collisions[8]; uint8_t _collisions[8];
enum OutputState {
Sync,
Blank,
ColourBurst,
Pixel
};
void output_pixels(unsigned int count); 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; Outputs::CRT::CRT *_crt;
Speaker _speaker;
// speaker backlog accumlation counter
unsigned int _cycles_since_speaker_update;
void update_audio();
// latched output state // latched output state
unsigned int _lastOutputStateDuration; unsigned int _lastOutputStateDuration;
OutputState _stateByExtendTime[2][57];
OutputState *_stateByTime;
OutputState _lastOutputState; OutputState _lastOutputState;
uint8_t *_outputBuffer; 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; return cycles;
} }
void Machine::update_output() void Machine::synchronise()
{ {
update_display(); update_display();
update_audio(); update_audio();

View File

@ -10,9 +10,8 @@
#define Electron_hpp #define Electron_hpp
#include "../../Processors/6502/CPU6502.hpp" #include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../Outputs/Speaker.hpp"
#include "../../Storage/Tape/Tape.hpp" #include "../../Storage/Tape/Tape.hpp"
#include "../CRTMachine.hpp"
#include <stdint.h> #include <stdint.h>
namespace Electron { namespace Electron {
@ -142,30 +141,34 @@ class Speaker: public ::Outputs::Filter<Speaker> {
@discussion An instance of Electron::Machine represents the current state of an @discussion An instance of Electron::Machine represents the current state of an
Acorn Electron. Acorn Electron.
*/ */
class Machine: public CPU6502::Processor<Machine>, Tape::Delegate { class Machine: public CPU6502::Processor<Machine>, public CRTMachine::Machine, Tape::Delegate {
public: public:
Machine(); 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_rom(ROMSlot slot, size_t length, const uint8_t *data);
void set_tape(std::shared_ptr<Storage::Tape> tape); void set_tape(std::shared_ptr<Storage::Tape> tape);
void set_key_state(Key key, bool isPressed); void set_key_state(Key key, bool isPressed);
void clear_all_keys(); 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; } 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: private:
inline void update_display(); inline void update_display();
@ -215,12 +218,12 @@ class Machine: public CPU6502::Processor<Machine>, Tape::Delegate {
uint8_t *_current_output_target, *_initial_output_target; uint8_t *_current_output_target, *_initial_output_target;
unsigned int _current_output_divider; unsigned int _current_output_divider;
// Tape. // Tape
Tape _tape; Tape _tape;
bool _use_fast_tape_hack; bool _use_fast_tape_hack;
bool _fast_load_is_in_data; bool _fast_load_is_in_data;
// Outputs. // Outputs
std::unique_ptr<Outputs::CRT::CRT> _crt; std::unique_ptr<Outputs::CRT::CRT> _crt;
Speaker _speaker; Speaker _speaker;
}; };

View File

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

View File

@ -10,17 +10,17 @@ import Cocoa
class Atari2600Document: MachineDocument { class Atari2600Document: MachineDocument {
private var atari2600 = CSAtari2600()
override func machine() -> CSMachine? {
return atari2600
}
// MARK: NSDocument overrides // MARK: NSDocument overrides
override init() { override init() {
super.init() super.init()
self.intendedCyclesPerSecond = 1194720 self.intendedCyclesPerSecond = 1194720
} }
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
atari2600.setView(openGLView, aspectRatio: 4.0 / 3.0)
}
override class func autosavesInPlace() -> Bool { override class func autosavesInPlace() -> Bool {
return true return true
} }
@ -31,7 +31,6 @@ class Atari2600Document: MachineDocument {
return "Atari2600Document" return "Atari2600Document"
} }
private var atari2600 = CSAtari2600()
override func dataOfType(typeName: String) throws -> NSData { 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. // 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. // 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) 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 // MARK: CSOpenGLViewResponderDelegate
private func inputForKey(event: NSEvent) -> Atari2600DigitalInput? { private func inputForKey(event: NSEvent) -> Atari2600DigitalInput? {

View File

@ -12,21 +12,26 @@ import AudioToolbox
class ElectronDocument: MachineDocument { class ElectronDocument: MachineDocument {
private lazy var electron = CSElectron() 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) { override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController) super.windowControllerDidLoadNib(aController)
self.intendedCyclesPerSecond = 2000000 self.intendedCyclesPerSecond = 2000000
aController.window?.contentAspectRatio = NSSize(width: 11.0, height: 10.0)
openGLView.performWithGLContext({ if let osPath = NSBundle.mainBundle().pathForResource("os", ofType: "rom") {
if let osPath = NSBundle.mainBundle().pathForResource("os", ofType: "rom") { self.electron.setOSROM(NSData(contentsOfFile: osPath)!)
self.electron.setOSROM(NSData(contentsOfFile: osPath)!) }
} if let basicPath = NSBundle.mainBundle().pathForResource("basic", ofType: "rom") {
if let basicPath = NSBundle.mainBundle().pathForResource("basic", ofType: "rom") { self.electron.setBASICROM(NSData(contentsOfFile: basicPath)!)
self.electron.setBASICROM(NSData(contentsOfFile: basicPath)!) }
}
self.electron.setView(self.openGLView, aspectRatio: 11.0 / 10.0)
self.electron.audioQueue = self.audioQueue
})
establishStoredOptions() establishStoredOptions()
} }
@ -58,19 +63,6 @@ class ElectronDocument: MachineDocument {
electron.setROM(data, slot: 15) 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 // MARK: IBActions
@IBOutlet var displayTypeButton: NSPopUpButton! @IBOutlet var displayTypeButton: NSPopUpButton!
@IBAction func setDisplayType(sender: NSPopUpButton!) { @IBAction func setDisplayType(sender: NSPopUpButton!) {
@ -102,21 +94,6 @@ class ElectronDocument: MachineDocument {
self.displayTypeButton.selectItemAtIndex(displayType) 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 // MARK: NSWindowDelegate
func windowDidResignKey(notification: NSNotification) { func windowDidResignKey(notification: NSNotification) {
electron.clearAllKeys() electron.clearAllKeys()

View File

@ -11,6 +11,16 @@ import AudioToolbox
class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDelegate, NSWindowDelegate { 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! { @IBOutlet weak var openGLView: CSOpenGLView! {
didSet { didSet {
openGLView.delegate = self openGLView.delegate = self
@ -23,13 +33,35 @@ class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDe
optionsPanel?.setIsVisible(true) optionsPanel?.setIsVisible(true)
} }
lazy var audioQueue = AudioQueue() var audioQueue : AudioQueue! = nil
override func windowControllerDidLoadNib(aController: NSWindowController) { override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController) super.windowControllerDidLoadNib(aController)
// bind the content aspect ratio to remain 4:3 from now on // establish the output aspect ratio and audio
aController.window?.contentAspectRatio = NSSize(width: 4.0, height: 3.0) 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 var intendedCyclesPerSecond: Int64 = 0
@ -54,16 +86,29 @@ class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDe
skippedFrames = 0 skippedFrames = 0
} }
if skippedFrames > 4 { // run for at most three frames up to and until that causes overshoots in the
numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency)) // 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)) runForNumberOfCycles(Int32(numberOfCycles))
} }
lastTime = time lastTime = time
} }
func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {} // MARK: CSOpenGLViewDelegate
func runForNumberOfCycles(numberOfCycles: Int32) {} 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 // MARK: CSOpenGLViewResponderDelegate
func keyDown(event: NSEvent) {} func keyDown(event: NSEvent) {}

View File

@ -14,6 +14,7 @@
CVDisplayLinkRef _displayLink; CVDisplayLinkRef _displayLink;
uint32_t _updateIsOngoing; uint32_t _updateIsOngoing;
BOOL _hasSkipped; BOOL _hasSkipped;
dispatch_queue_t _serialDispatchQueue;
} }
- (void)prepareOpenGL - (void)prepareOpenGL
@ -33,6 +34,10 @@
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj]; CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat); 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 // set the clear colour
[self.openGLContext makeCurrentContext]; [self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0); 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 - (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency
{ {
const uint32_t processingMask = 0x01; 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)) if(!OSAtomicTestAndSet(processingMask, &_updateIsOngoing))
{ {
CVTimeStamp time = *now; CVTimeStamp time = *now;
BOOL didSkip = _hasSkipped; 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.delegate openGLView:self didUpdateToTime:time didSkipPreviousUpdate:didSkip frequency:frequency];
[self drawViewOnlyIfDirty:YES];
OSAtomicTestAndClear(processingMask, &_updateIsOngoing); OSAtomicTestAndClear(processingMask, &_updateIsOngoing);
}); });
_hasSkipped = NO; _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 - (void)invalidate

View File

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

View File

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

View File

@ -15,6 +15,4 @@
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput; - (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput;
- (void)setResetLineEnabled:(BOOL)enabled; - (void)setResetLineEnabled:(BOOL)enabled;
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
@end @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 { - (void)setROM:(NSData *)rom {
@synchronized(self) { @synchronized(self) {
_atari2600.set_rom(rom.length, (const uint8_t *)rom.bytes); _atari2600.set_rom(rom.length, (const uint8_t *)rom.bytes);
@ -79,16 +69,14 @@ struct CRTDelegate: public Outputs::CRT::Delegate {
- (void)setupOutputWithAspectRatio:(float)aspectRatio { - (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) { @synchronized(self) {
_atari2600.setup_output(aspectRatio); [super setupOutputWithAspectRatio:aspectRatio];
_atari2600.get_crt()->set_delegate(&_crtDelegate); _atari2600.get_crt()->set_delegate(&_crtDelegate);
_crtDelegate.atari2600 = self; _crtDelegate.atari2600 = self;
} }
} }
- (void)closeOutput { - (CRTMachine::Machine * const)machine {
@synchronized(self) { return &_atari2600;
_atari2600.close_output();
}
} }
@end @end

View File

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

View File

@ -16,11 +16,8 @@
Electron::Machine _electron; Electron::Machine _electron;
} }
- (void)runForNumberOfCycles:(int)numberOfCycles { - (CRTMachine::Machine * const)machine {
@synchronized(self) { return &_electron;
_electron.run_for_cycles(numberOfCycles);
_electron.update_output();
}
} }
- (void)setOSROM:(nonnull NSData *)rom { - (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 { - (BOOL)openUEFAtURL:(NSURL *)URL {
@synchronized(self) { @synchronized(self) {
try { try {
@ -57,24 +50,13 @@
} }
} }
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate { - (void)clearAllKeys {
@synchronized(self) { @synchronized(self) {
_electron.get_speaker()->set_output_rate(sampleRate, 256); _electron.clear_all_keys();
_electron.get_speaker()->set_delegate(delegate);
return YES;
} }
} }
- (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 { - (void)setKey:(uint16_t)key isPressed:(BOOL)isPressed {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
@synchronized(self) { @synchronized(self) {
switch(key) switch(key)
{ {
@ -152,16 +134,13 @@
break; break;
} }
} }
// });
} }
- (void)setUseFastLoadingHack:(BOOL)useFastLoadingHack { - (void)setUseFastLoadingHack:(BOOL)useFastLoadingHack {
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ @synchronized(self) {
@synchronized(self) { _useFastLoadingHack = useFastLoadingHack;
_useFastLoadingHack = useFastLoadingHack; _electron.set_use_fast_tape_hack(useFastLoadingHack ? true : false);
_electron.set_use_fast_tape_hack(useFastLoadingHack ? true : false); }
}
// });
} }
- (void)setUseTelevisionOutput:(BOOL)useTelevisionOutput { - (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 @end

View File

@ -7,16 +7,15 @@
// //
#import "CSMachine.h" #import "CSMachine.h"
#include "CRT.hpp" #include "CRTMachine.hpp"
#include "Speaker.hpp"
@interface CSMachine (Subclassing) @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)performAsync:(dispatch_block_t)action;
- (void)performSync:(dispatch_block_t)action; - (void)performSync:(dispatch_block_t)action;
- (CRTMachine::Machine * const)machine;
- (void)setupOutputWithAspectRatio:(float)aspectRatio; - (void)setupOutputWithAspectRatio:(float)aspectRatio;
- (void)closeOutput;
@end @end

View File

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

View File

@ -9,6 +9,10 @@
#import "CSMachine.h" #import "CSMachine.h"
#import "CSMachine+Subclassing.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 { struct SpeakerDelegate: public Outputs::Speaker::Delegate {
__weak CSMachine *machine; __weak CSMachine *machine;
void speaker_did_complete_samples(Outputs::Speaker *speaker, const int16_t *buffer, int buffer_size) { 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) { if(self) {
_serialDispatchQueue = dispatch_queue_create("Machine queue", DISPATCH_QUEUE_SERIAL); _serialDispatchQueue = dispatch_queue_create("Machine queue", DISPATCH_QUEUE_SERIAL);
_speakerDelegate.machine = self;
[self setSpeakerDelegate:&_speakerDelegate sampleRate:44100];
} }
return self; return self;
@ -39,15 +41,48 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
- (void)dealloc { - (void)dealloc {
[_view performWithGLContext:^{ [_view performWithGLContext:^{
[self closeOutput]; @synchronized(self) {
self.machine->close_output();
}
}]; }];
} }
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate { - (int)idealSamplingRateFromRange:(NSRange)range {
return NO; @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 { - (void)performSync:(dispatch_block_t)action {
dispatch_sync(_serialDispatchQueue, action); dispatch_sync(_serialDispatchQueue, action);
@ -57,10 +92,6 @@ struct SpeakerDelegate: public Outputs::Speaker::Delegate {
dispatch_async(_serialDispatchQueue, action); dispatch_async(_serialDispatchQueue, action);
} }
- (void)setupOutputWithAspectRatio:(float)aspectRatio {}
- (void)closeOutput {}
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio { - (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
_view = view; _view = view;
[view performWithGLContext:^{ [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 @end

View File

@ -108,6 +108,7 @@ namespace {
OpenGLOutputBuilder::OpenGLOutputBuilder(unsigned int buffer_depth) : OpenGLOutputBuilder::OpenGLOutputBuilder(unsigned int buffer_depth) :
_output_mutex(new std::mutex), _output_mutex(new std::mutex),
_draw_mutex(new std::mutex),
_visible_area(Rect(0, 0, 1, 1)), _visible_area(Rect(0, 0, 1, 1)),
_composite_src_output_y(0), _composite_src_output_y(0),
_cleared_composite_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) 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 // lock down any other draw_frames
_output_mutex->lock(); _draw_mutex->lock();
// establish essentials // establish essentials
if(!output_shader_program) if(!output_shader_program)
@ -195,21 +196,6 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
glDeleteSync(_fence); 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 // make sure there's a target to draw to
if(!framebuffer || framebuffer->get_height() != output_height || framebuffer->get_width() != output_width) 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); 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 // upload new source pixels
if(completed_texture_y) if(completed_texture_y)
{ {
@ -239,6 +243,9 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
_buffer_builder->get_image_pointer()); _buffer_builder->get_image_pointer());
} }
// data having been grabbed, allow the machine to continue
_output_mutex->unlock();
struct RenderStage { struct RenderStage {
OpenGL::TextureTarget *const target; OpenGL::TextureTarget *const target;
OpenGL::Shader *const shader; 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); framebuffer->draw((float)output_width / (float)output_height);
_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); _fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
_output_mutex->unlock(); _draw_mutex->unlock();
} }
void OpenGLOutputBuilder::reset_all_OpenGL_state() void OpenGLOutputBuilder::reset_all_OpenGL_state()

View File

@ -58,6 +58,7 @@ class OpenGLOutputBuilder {
// the run and input data buffers // the run and input data buffers
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder; std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
std::unique_ptr<std::mutex> _output_mutex; std::unique_ptr<std::mutex> _output_mutex;
std::unique_ptr<std::mutex> _draw_mutex;
// transient buffers indicating composite data not yet decoded // transient buffers indicating composite data not yet decoded
GLsizei _composite_src_output_y, _cleared_composite_output_y; 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(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;" "fragColour = lumaChromaToRGB * lumaChromaColourInRange;"
"}", false, false); "}", false, false);
} }

View File

@ -78,7 +78,7 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
"void main(void)" "void main(void)"
"{" "{"
"fragColour = vec4(%s, 0.5*cos(lateralVarying));" "fragColour = vec4(%s, 0.5);"//*cos(lateralVarying)
"}", "}",
sampler_type, fragment_methods, colour_expression); 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; 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) void set_output_rate(int cycles_per_second, int buffer_size)
{ {
_output_cycles_per_second = cycles_per_second; _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(); if(_coefficients_are_dirty) update_filter_coefficients();
// TODO: what if output rate is greater than input rate? // if input and output rates exactly match, just accumulate results and pass on
if(_input_cycles_per_second == _output_cycles_per_second)
// fill up as much of the input buffer as possible
while(input_cycles)
{ {
unsigned int cycles_to_read = (unsigned int)std::min((int)input_cycles, _number_of_taps - _input_buffer_depth); while(input_cycles)
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)
{ {
_buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get()); unsigned int cycles_to_read = (unsigned int)(_buffer_size - _buffer_in_progress_pointer);
_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 // announce to delegate if full
if(_buffer_in_progress_pointer == _buffer_size) 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 input_cycles -= cycles_to_read;
// 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(); return;
if(steps < _number_of_taps) }
// 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(); _buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get());
memmove(input_buffer, &input_buffer[steps], sizeof(int16_t) * ((size_t)_number_of_taps - (size_t)steps)); _buffer_in_progress_pointer++;
_input_buffer_depth -= steps;
} // announce to delegate if full
else if(_buffer_in_progress_pointer == _buffer_size)
{ {
if(steps > _number_of_taps) _buffer_in_progress_pointer = 0;
static_cast<T *>(this)->skip_samples((unsigned int)steps - (unsigned int)_number_of_taps); if(_delegate)
_input_buffer_depth = 0; {
_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: 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. @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 @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 order to provide the bus on which the 6502 operates and @c synchronise(), which is called upon completion of a continuous run
a jam handler and inserting jam opcodes where appropriate; that will cause call outs when the program counter reaches those of cycles to allow a subclass to bring any on-demand activities up to date.
addresses. @c return_from_subroutine can be used to exit from a jammed state.
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 { template <class T> class Processor {
public: public:
@ -597,6 +600,7 @@ template <class T> class Processor {
case CycleFetchOperation: { case CycleFetchOperation: {
_lastOperationPC = _pc; _lastOperationPC = _pc;
// printf("%04x x:%02x\n", _pc.full, _x);
_pc.full++; _pc.full++;
read_op(_operation, _lastOperationPC.full); read_op(_operation, _lastOperationPC.full);
@ -1040,15 +1044,17 @@ template <class T> class Processor {
_ready_is_active = true; _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();
} }
/*! /*!