diff --git a/Machines/Atari2600/Atari2600.cpp b/Machines/Atari2600/Atari2600.cpp index 835352a92..e479780f5 100644 --- a/Machines/Atari2600/Atari2600.cpp +++ b/Machines/Atari2600/Atari2600.cpp @@ -11,7 +11,9 @@ #include 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) +{ +} diff --git a/Machines/Atari2600/Atari2600.hpp b/Machines/Atari2600/Atari2600.hpp index 12b0c4d9e..656cc844d 100644 --- a/Machines/Atari2600/Atari2600.hpp +++ b/Machines/Atari2600/Atari2600.hpp @@ -10,28 +10,64 @@ #define Atari2600_cpp #include "../../Processors/6502/CPU6502.hpp" -#include "../../Outputs/CRT/CRT.hpp" +#include "../CRTMachine.hpp" #include #include "Atari2600Inputs.h" namespace Atari2600 { -class Machine: public CPU6502::Processor { +const unsigned int number_of_upcoming_events = 6; +const unsigned int number_of_recorded_counters = 7; + +class Speaker: public ::Outputs::Filter { + 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, 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::run_for_cycles(number_of_cycles); } private: uint8_t *_rom, *_romPages[4], _ram[128]; @@ -46,32 +82,81 @@ class Machine: public CPU6502::Processor { 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 { // 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(); }; } diff --git a/Machines/CRTMachine.hpp b/Machines/CRTMachine.hpp new file mode 100644 index 000000000..235e1e10b --- /dev/null +++ b/Machines/CRTMachine.hpp @@ -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 */ diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp index ff1b5ab93..af60bb8de 100644 --- a/Machines/Electron/Electron.cpp +++ b/Machines/Electron/Electron.cpp @@ -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(); diff --git a/Machines/Electron/Electron.hpp b/Machines/Electron/Electron.hpp index 151cdbe54..d249bac0d 100644 --- a/Machines/Electron/Electron.hpp +++ b/Machines/Electron/Electron.hpp @@ -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 namespace Electron { @@ -142,30 +141,34 @@ class Speaker: public ::Outputs::Filter { @discussion An instance of Electron::Machine represents the current state of an Acorn Electron. */ -class Machine: public CPU6502::Processor, Tape::Delegate { +class Machine: public CPU6502::Processor, 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 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::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, 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 _crt; Speaker _speaker; }; diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 5b2fa6348..f9dac9acd 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTConstants.hpp; sourceTree = ""; }; 4B0CCC421C62D0B3001CAC5F /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRT.cpp; sourceTree = ""; }; 4B0CCC431C62D0B3001CAC5F /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRT.hpp; sourceTree = ""; }; @@ -1192,6 +1193,7 @@ children = ( 4B2E2D961C3A06EC00138695 /* Atari2600 */, 4B2E2D9E1C3A070900138695 /* Electron */, + 4B046DC31CFE651500E9E45E /* CRTMachine.hpp */, ); name = Machines; path = ../../Machines; diff --git a/OSBindings/Mac/Clock Signal/Documents/Atari2600Document.swift b/OSBindings/Mac/Clock Signal/Documents/Atari2600Document.swift index 6c586c360..e13fff8fa 100644 --- a/OSBindings/Mac/Clock Signal/Documents/Atari2600Document.swift +++ b/OSBindings/Mac/Clock Signal/Documents/Atari2600Document.swift @@ -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? { diff --git a/OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift b/OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift index 4d08e603a..9f73af395 100644 --- a/OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift @@ -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() diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 473e81aad..e752f032f 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -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) {} diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index ea66b245c..ce4a19046 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h b/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h index cf96e1166..de14ea68b 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h +++ b/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m b/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m index 80f14d7e0..9a409ea60 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m +++ b/OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.h b/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.h index c0e097c52..05a0b70c3 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.h +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.h @@ -15,6 +15,4 @@ - (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput; - (void)setResetLineEnabled:(BOOL)enabled; -- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty; - @end diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm b/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm index 4dab3724c..cec8e85bf 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h b/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h index 3b4d45ff3..04d8e51ac 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h @@ -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; diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm b/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm index bb8ec8c12..c7b82425e 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h index 96d090697..59f724bb2 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h @@ -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 diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h index 89467de11..c91487b7f 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h @@ -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; diff --git a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm index 7c5507218..a52d68bec 100644 --- a/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm @@ -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 diff --git a/Outputs/CRT/Internals/CRTOpenGL.cpp b/Outputs/CRT/Internals/CRTOpenGL.cpp index 94df2e3b6..e01ea78a5 100644 --- a/Outputs/CRT/Internals/CRTOpenGL.cpp +++ b/Outputs/CRT/Internals/CRTOpenGL.cpp @@ -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() diff --git a/Outputs/CRT/Internals/CRTOpenGL.hpp b/Outputs/CRT/Internals/CRTOpenGL.hpp index e562767a8..4f0a79a32 100644 --- a/Outputs/CRT/Internals/CRTOpenGL.hpp +++ b/Outputs/CRT/Internals/CRTOpenGL.hpp @@ -58,6 +58,7 @@ class OpenGLOutputBuilder { // the run and input data buffers std::unique_ptr _buffer_builder; std::unique_ptr _output_mutex; + std::unique_ptr _draw_mutex; // transient buffers indicating composite data not yet decoded GLsizei _composite_src_output_y, _cleared_composite_output_y; diff --git a/Outputs/CRT/Internals/Shaders/IntermediateShader.cpp b/Outputs/CRT/Internals/Shaders/IntermediateShader.cpp index ed82601ea..8d15a1291 100644 --- a/Outputs/CRT/Internals/Shaders/IntermediateShader.cpp +++ b/Outputs/CRT/Internals/Shaders/IntermediateShader.cpp @@ -269,7 +269,7 @@ std::unique_ptr 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); } diff --git a/Outputs/CRT/Internals/Shaders/OutputShader.cpp b/Outputs/CRT/Internals/Shaders/OutputShader.cpp index 682e62304..f543853a7 100644 --- a/Outputs/CRT/Internals/Shaders/OutputShader.cpp +++ b/Outputs/CRT/Internals/Shaders/OutputShader.cpp @@ -78,7 +78,7 @@ std::unique_ptr 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); diff --git a/Outputs/Speaker.hpp b/Outputs/Speaker.hpp index 759d04472..9742e86df 100644 --- a/Outputs/Speaker.hpp +++ b/Outputs/Speaker.hpp @@ -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 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(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(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 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(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(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(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: diff --git a/Processors/6502/CPU6502.hpp b/Processors/6502/CPU6502.hpp index f5d731daa..e4e884a06 100644 --- a/Processors/6502/CPU6502.hpp +++ b/Processors/6502/CPU6502.hpp @@ -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 Processor { public: @@ -597,6 +600,7 @@ template 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 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(this)->synchronise(); } /*!