mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-26 23:52:26 +00:00
Merge pull request #17 from TomHarte/2600Accuracy
Made an attempt substantially to improve Atari 2600 emulation accuracy
This commit is contained in:
commit
4a507b375b
@ -11,7 +11,9 @@
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace Atari2600;
|
||||
static const int horizontalTimerReload = 227;
|
||||
namespace {
|
||||
static const unsigned int horizontalTimerPeriod = 228;
|
||||
}
|
||||
|
||||
Machine::Machine() :
|
||||
_horizontalTimer(0),
|
||||
@ -19,12 +21,39 @@ Machine::Machine() :
|
||||
_lastOutputState(OutputState::Sync),
|
||||
_piaTimerStatus(0xff),
|
||||
_rom(nullptr),
|
||||
_hMoveWillCount(false),
|
||||
_piaDataValue{0xff, 0xff},
|
||||
_tiaInputValue{0xff, 0xff}
|
||||
_tiaInputValue{0xff, 0xff},
|
||||
_upcomingEventsPointer(0),
|
||||
_objectCounterPointer(0),
|
||||
_stateByTime(_stateByExtendTime[0]),
|
||||
_cycles_since_speaker_update(0)
|
||||
{
|
||||
memset(_collisions, 0xff, sizeof(_collisions));
|
||||
set_reset_line(true);
|
||||
setup_reported_collisions();
|
||||
|
||||
for(int vbextend = 0; vbextend < 2; vbextend++)
|
||||
{
|
||||
for(int c = 0; c < 57; c++)
|
||||
{
|
||||
OutputState state;
|
||||
|
||||
// determine which output state will be active in four cycles from now
|
||||
switch(c)
|
||||
{
|
||||
case 0: case 1: case 2: case 3: state = OutputState::Blank; break;
|
||||
case 4: case 5: case 6: case 7: state = OutputState::Sync; break;
|
||||
case 8: case 9: case 10: case 11: state = OutputState::ColourBurst; break;
|
||||
case 12: case 13: case 14:
|
||||
case 15: case 16: state = OutputState::Blank; break;
|
||||
|
||||
case 17: case 18: state = vbextend ? OutputState::Blank : OutputState::Pixel; break;
|
||||
default: state = OutputState::Pixel; break;
|
||||
}
|
||||
|
||||
_stateByExtendTime[vbextend][c] = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Machine::setup_output(float aspect_ratio)
|
||||
@ -43,6 +72,8 @@ void Machine::setup_output(float aspect_ratio)
|
||||
"return (float(y) / 14.0) * (1.0 - amplitude) + step(1, iPhase) * amplitude * cos(phase + phaseOffset);"
|
||||
"}");
|
||||
_crt->set_output_device(Outputs::CRT::Television);
|
||||
|
||||
_speaker.set_input_rate(1194720 / 38);
|
||||
}
|
||||
|
||||
void Machine::switch_region()
|
||||
@ -61,6 +92,8 @@ void Machine::switch_region()
|
||||
"return (float(y) / 14.0) * (1.0 - amplitude) + step(4, (iPhase + 2u) & 15u) * amplitude * cos(phase + phaseOffset);"
|
||||
"}");
|
||||
_crt->set_new_timing(228, 312, Outputs::CRT::ColourSpace::YUV, 228, 1);
|
||||
|
||||
// _speaker.set_input_rate(2 * 312 * 50);
|
||||
}
|
||||
|
||||
void Machine::close_output()
|
||||
@ -75,96 +108,133 @@ Machine::~Machine()
|
||||
close_output();
|
||||
}
|
||||
|
||||
void Machine::get_output_pixel(uint8_t *pixel, int offset)
|
||||
void Machine::update_timers(int mask)
|
||||
{
|
||||
// get the playfield pixel and hence a proposed colour
|
||||
uint8_t playfieldPixel = _playfield[offset >> 2];
|
||||
uint8_t playfieldColour = ((_playfieldControl&6) == 2) ? _playerColour[offset / 80] : _playfieldColour;
|
||||
unsigned int upcomingPointerPlus4 = (_upcomingEventsPointer + 4)%number_of_upcoming_events;
|
||||
|
||||
// get player and missile proposed pixels
|
||||
uint8_t playerPixels[2] = {0, 0}, missilePixels[2] = {0, 0};
|
||||
for(int c = 0; c < 2; c++)
|
||||
_objectCounterPointer = (_objectCounterPointer + 1)%number_of_recorded_counters;
|
||||
ObjectCounter *oneClockAgo = _objectCounter[(_objectCounterPointer - 1 + number_of_recorded_counters)%number_of_recorded_counters];
|
||||
ObjectCounter *twoClocksAgo = _objectCounter[(_objectCounterPointer - 2 + number_of_recorded_counters)%number_of_recorded_counters];
|
||||
ObjectCounter *now = _objectCounter[_objectCounterPointer];
|
||||
|
||||
// grab the background now, for application in four clocks
|
||||
if(mask & (1 << 5) && !(_horizontalTimer&3))
|
||||
{
|
||||
const uint8_t repeatMask = _playerAndMissileSize[c]&7;
|
||||
if(_playerGraphics[c]) {
|
||||
// figure out player colour
|
||||
int flipMask = (_playerReflection[c]&0x8) ? 0 : 7;
|
||||
|
||||
int relativeTimer = _objectCounter[c] - 5;
|
||||
switch (repeatMask)
|
||||
{
|
||||
case 0: break;
|
||||
default:
|
||||
if(repeatMask&4 && relativeTimer >= 64) relativeTimer -= 64;
|
||||
else if(repeatMask&2 && relativeTimer >= 32) relativeTimer -= 32;
|
||||
else if(repeatMask&1 && relativeTimer >= 16) relativeTimer -= 16;
|
||||
break;
|
||||
case 5:
|
||||
relativeTimer >>= 1;
|
||||
break;
|
||||
case 7:
|
||||
relativeTimer >>= 2;
|
||||
break;
|
||||
}
|
||||
|
||||
if(relativeTimer >= 0 && relativeTimer < 8)
|
||||
playerPixels[c] = (_playerGraphics[c] >> (relativeTimer ^ flipMask)) &1;
|
||||
}
|
||||
|
||||
// figure out missile colour
|
||||
if((_missileGraphicsEnable[c]&2) && !(_missileGraphicsReset[c]&2)) {
|
||||
int missileIndex = _objectCounter[2+c] - 4;
|
||||
switch (repeatMask)
|
||||
{
|
||||
case 0: break;
|
||||
default:
|
||||
if(repeatMask&4 && missileIndex >= 64) missileIndex -= 64;
|
||||
else if(repeatMask&2 && missileIndex >= 32) missileIndex -= 32;
|
||||
else if(repeatMask&1 && missileIndex >= 16) missileIndex -= 16;
|
||||
break;
|
||||
case 5:
|
||||
missileIndex >>= 1;
|
||||
break;
|
||||
case 7:
|
||||
missileIndex >>= 2;
|
||||
break;
|
||||
}
|
||||
int missileSize = 1 << ((_playerAndMissileSize[c] >> 4)&3);
|
||||
missilePixels[c] = (missileIndex >= 0 && missileIndex < missileSize) ? 1 : 0;
|
||||
}
|
||||
unsigned int offset = 4 + _horizontalTimer - (horizontalTimerPeriod - 160);
|
||||
_upcomingEvents[upcomingPointerPlus4].updates |= Event::Action::Playfield;
|
||||
_upcomingEvents[upcomingPointerPlus4].playfieldPixel = _playfield[(offset >> 2)%40];
|
||||
}
|
||||
|
||||
// get the ball proposed colour
|
||||
if(mask & (1 << 4))
|
||||
{
|
||||
// the ball becomes visible whenever it hits zero, regardless of whether its status
|
||||
// is the result of a counter rollover or a programmatic reset, and there's a four
|
||||
// clock delay on that triggering the start signal
|
||||
now[4].count = (oneClockAgo[4].count + 1)%160;
|
||||
now[4].pixel = oneClockAgo[4].pixel + 1;
|
||||
if(!now[4].count) now[4].pixel = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
now[4] = oneClockAgo[4];
|
||||
}
|
||||
|
||||
// check for player and missle triggers
|
||||
for(int c = 0; c < 4; c++)
|
||||
{
|
||||
if(mask & (1 << c))
|
||||
{
|
||||
// update the count
|
||||
now[c].count = (oneClockAgo[c].count + 1)%160;
|
||||
|
||||
uint8_t repeatMask = _playerAndMissileSize[c&1] & 7;
|
||||
ObjectCounter *rollover;
|
||||
ObjectCounter *equality;
|
||||
|
||||
if(c < 2)
|
||||
{
|
||||
// update the pixel
|
||||
now[c].broad_pixel = oneClockAgo[c].broad_pixel + 1;
|
||||
switch(repeatMask)
|
||||
{
|
||||
default: now[c].pixel = oneClockAgo[c].pixel + 1; break;
|
||||
case 5: now[c].pixel = oneClockAgo[c].pixel + (now[c].broad_pixel&1); break;
|
||||
case 7: now[c].pixel = oneClockAgo[c].pixel + (((now[c].broad_pixel | (now[c].broad_pixel >> 1))^1)&1); break;
|
||||
}
|
||||
|
||||
// check for a rollover six clocks ago or equality five clocks ago
|
||||
rollover = twoClocksAgo;
|
||||
equality = oneClockAgo;
|
||||
}
|
||||
else
|
||||
{
|
||||
// update the pixel
|
||||
now[c].pixel = oneClockAgo[c].pixel + 1;
|
||||
|
||||
// check for a rollover five clocks ago or equality four clocks ago
|
||||
rollover = oneClockAgo;
|
||||
equality = now;
|
||||
}
|
||||
|
||||
if(
|
||||
(rollover[c].count == 159) ||
|
||||
(_hasSecondCopy[c&1] && equality[c].count == 16) ||
|
||||
(_hasThirdCopy[c&1] && equality[c].count == 32) ||
|
||||
(_hasFourthCopy[c&1] && equality[c].count == 64)
|
||||
)
|
||||
{
|
||||
now[c].pixel = 0;
|
||||
now[c].broad_pixel = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
now[c] = oneClockAgo[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Machine::get_output_pixel()
|
||||
{
|
||||
ObjectCounter *now = _objectCounter[_objectCounterPointer];
|
||||
|
||||
// get the playfield pixel
|
||||
unsigned int offset = _horizontalTimer - (horizontalTimerPeriod - 160);
|
||||
uint8_t playfieldColour = ((_playfieldControl&6) == 2) ? _playerColour[offset / 80] : _playfieldColour;
|
||||
|
||||
// ball pixel
|
||||
uint8_t ballPixel = 0;
|
||||
if(_ballGraphicsEnable&2) {
|
||||
int ballIndex = _objectCounter[4] - 4;
|
||||
int ballSize = 1 << ((_playfieldControl >> 4)&3);
|
||||
ballPixel = (ballIndex >= 0 && ballIndex < ballSize) ? 1 : 0;
|
||||
if(now[4].pixel < _ballSize) {
|
||||
ballPixel = _ballGraphicsEnable[_ballGraphicsSelector];
|
||||
}
|
||||
|
||||
// determine the player and missile pixels
|
||||
uint8_t playerPixels[2] = { 0, 0 };
|
||||
uint8_t missilePixels[2] = { 0, 0 };
|
||||
for(int c = 0; c < 2; c++)
|
||||
{
|
||||
if(_playerGraphics[c] && now[c].pixel < 8) {
|
||||
playerPixels[c] = (_playerGraphics[_playerGraphicsSelector[c]][c] >> (now[c].pixel ^ _playerReflectionMask[c])) & 1;
|
||||
}
|
||||
|
||||
if(!_missileGraphicsReset[c] && now[c+2].pixel < _missileSize[c]) {
|
||||
missilePixels[c] = _missileGraphicsEnable[c];
|
||||
}
|
||||
}
|
||||
|
||||
// accumulate collisions
|
||||
if(playerPixels[0] | playerPixels[1]) {
|
||||
_collisions[0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6);
|
||||
_collisions[1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6);
|
||||
|
||||
_collisions[2] |= ((playfieldPixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6);
|
||||
_collisions[3] |= ((playfieldPixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6);
|
||||
|
||||
_collisions[7] |= ((playerPixels[0] & playerPixels[1]) << 7);
|
||||
}
|
||||
|
||||
if(playfieldPixel | ballPixel) {
|
||||
_collisions[4] |= ((playfieldPixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
|
||||
_collisions[5] |= ((playfieldPixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
|
||||
|
||||
_collisions[6] |= ((playfieldPixel & ballPixel) << 7);
|
||||
}
|
||||
|
||||
if(missilePixels[0] & missilePixels[1])
|
||||
_collisions[7] |= (1 << 6);
|
||||
int pixel_mask = playerPixels[0] | (playerPixels[1] << 1) | (missilePixels[0] << 2) | (missilePixels[1] << 3) | (ballPixel << 4) | (_playfieldOutput << 5);
|
||||
_collisions[0] |= _reportedCollisions[pixel_mask][0];
|
||||
_collisions[1] |= _reportedCollisions[pixel_mask][1];
|
||||
_collisions[2] |= _reportedCollisions[pixel_mask][2];
|
||||
_collisions[3] |= _reportedCollisions[pixel_mask][3];
|
||||
_collisions[4] |= _reportedCollisions[pixel_mask][4];
|
||||
_collisions[5] |= _reportedCollisions[pixel_mask][5];
|
||||
_collisions[6] |= _reportedCollisions[pixel_mask][6];
|
||||
_collisions[7] |= _reportedCollisions[pixel_mask][7];
|
||||
|
||||
// apply appropriate priority to pick a colour
|
||||
playfieldPixel |= ballPixel;
|
||||
uint8_t playfieldPixel = _playfieldOutput | ballPixel;
|
||||
uint8_t outputColour = playfieldPixel ? playfieldColour : _backgroundColour;
|
||||
|
||||
if(!(_playfieldControl&0x04) || !playfieldPixel) {
|
||||
@ -172,73 +242,131 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
|
||||
if(playerPixels[0] || missilePixels[0]) outputColour = _playerColour[0];
|
||||
}
|
||||
|
||||
// store colour
|
||||
// static int lc;
|
||||
// if(_vSyncEnabled) lc = 0; else lc += (offset == 159) ? 1 : 0;
|
||||
// *pixel = (uint8_t)(((offset / 10) << 4) | (((lc >> 4)&7) << 1));
|
||||
*pixel = outputColour;
|
||||
// return colour
|
||||
return outputColour;
|
||||
}
|
||||
|
||||
// in imputing the knowledge that all we're dealing with is the rollover from 159 to 0,
|
||||
// this is faster than the straightforward +1)%160 per profiling
|
||||
#define increment_object_counter(c) _objectCounter[c] = (_objectCounter[c]+1)&~((158-_objectCounter[c]) >> 8)
|
||||
void Machine::setup_reported_collisions()
|
||||
{
|
||||
for(int c = 0; c < 64; c++)
|
||||
{
|
||||
memset(_reportedCollisions[c], 0, 8);
|
||||
|
||||
int playerPixels[2] = { c&1, (c >> 1)&1 };
|
||||
int missilePixels[2] = { (c >> 2)&1, (c >> 3)&1 };
|
||||
int ballPixel = (c >> 4)&1;
|
||||
int playfieldPixel = (c >> 5)&1;
|
||||
|
||||
if(playerPixels[0] | playerPixels[1]) {
|
||||
_reportedCollisions[c][0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6);
|
||||
_reportedCollisions[c][1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6);
|
||||
|
||||
_reportedCollisions[c][2] |= ((playfieldPixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6);
|
||||
_reportedCollisions[c][3] |= ((playfieldPixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6);
|
||||
|
||||
_reportedCollisions[c][7] |= ((playerPixels[0] & playerPixels[1]) << 7);
|
||||
}
|
||||
|
||||
if(playfieldPixel | ballPixel) {
|
||||
_reportedCollisions[c][4] |= ((playfieldPixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
|
||||
_reportedCollisions[c][5] |= ((playfieldPixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
|
||||
|
||||
_reportedCollisions[c][6] |= ((playfieldPixel & ballPixel) << 7);
|
||||
}
|
||||
|
||||
if(missilePixels[0] & missilePixels[1])
|
||||
_reportedCollisions[c][7] |= (1 << 6);
|
||||
}
|
||||
}
|
||||
|
||||
void Machine::output_pixels(unsigned int count)
|
||||
{
|
||||
const int32_t start_of_sync = 214;
|
||||
const int32_t end_of_sync = 198;
|
||||
const int32_t end_of_colour_burst = 188;
|
||||
|
||||
while(count--)
|
||||
{
|
||||
OutputState state;
|
||||
if(_upcomingEvents[_upcomingEventsPointer].updates)
|
||||
{
|
||||
// apply any queued changes and flush the record
|
||||
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveSetup)
|
||||
{
|
||||
// schedule an extended left border
|
||||
_stateByTime = _stateByExtendTime[1];
|
||||
|
||||
// update hmove
|
||||
if(!(_horizontalTimer&3)) {
|
||||
// clear any ongoing moves
|
||||
if(_hMoveFlags)
|
||||
{
|
||||
for(int c = 0; c < number_of_upcoming_events; c++)
|
||||
{
|
||||
_upcomingEvents[c].updates &= ~(Event::Action::HMoveCompare | Event::Action::HMoveDecrement);
|
||||
}
|
||||
}
|
||||
|
||||
if(_hMoveFlags) {
|
||||
const uint8_t counterValue = _hMoveCounter ^ 0x7;
|
||||
for(int c = 0; c < 5; c++) {
|
||||
if(counterValue == (_objectMotion[c] >> 4)) _hMoveFlags &= ~(1 << c);
|
||||
if(_hMoveFlags&(1 << c)) increment_object_counter(c);
|
||||
// schedule new moves
|
||||
_hMoveFlags = 0x1f;
|
||||
_hMoveCounter = 15;
|
||||
|
||||
// follow-through into a compare immediately
|
||||
_upcomingEvents[_upcomingEventsPointer].updates |= Event::Action::HMoveCompare;
|
||||
}
|
||||
|
||||
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveCompare)
|
||||
{
|
||||
for(int c = 0; c < 5; c++)
|
||||
{
|
||||
if(((_objectMotion[c] >> 4)^_hMoveCounter) == 7)
|
||||
{
|
||||
_hMoveFlags &= ~(1 << c);
|
||||
}
|
||||
}
|
||||
if(_hMoveFlags)
|
||||
{
|
||||
if(_hMoveCounter) _hMoveCounter--;
|
||||
_upcomingEvents[(_upcomingEventsPointer+4)%number_of_upcoming_events].updates |= Event::Action::HMoveCompare;
|
||||
_upcomingEvents[(_upcomingEventsPointer+2)%number_of_upcoming_events].updates |= Event::Action::HMoveDecrement;
|
||||
}
|
||||
}
|
||||
|
||||
if(_hMoveIsCounting) {
|
||||
_hMoveIsCounting = !!_hMoveCounter;
|
||||
_hMoveCounter = (_hMoveCounter-1)&0xf;
|
||||
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::HMoveDecrement)
|
||||
{
|
||||
update_timers(_hMoveFlags);
|
||||
}
|
||||
|
||||
if(_upcomingEvents[_upcomingEventsPointer].updates & Event::Action::ResetCounter)
|
||||
{
|
||||
_objectCounter[_objectCounterPointer][_upcomingEvents[_upcomingEventsPointer].counter].count = 0;
|
||||
}
|
||||
|
||||
// zero out current update event
|
||||
_upcomingEvents[_upcomingEventsPointer].updates = 0;
|
||||
}
|
||||
|
||||
// progress to next event
|
||||
_upcomingEventsPointer = (_upcomingEventsPointer + 1)%number_of_upcoming_events;
|
||||
|
||||
// blank is decoded as 68 counts; sync and colour burst as 16 counts
|
||||
// determine which output state is currently active
|
||||
OutputState primary_state = _stateByTime[_horizontalTimer >> 2];
|
||||
OutputState effective_state = primary_state;
|
||||
|
||||
// 4 blank
|
||||
// 4 sync
|
||||
// 9 'blank'; colour burst after 4
|
||||
// 40 pixels
|
||||
// update pixel timers
|
||||
if(primary_state == OutputState::Pixel) update_timers(~0);
|
||||
|
||||
// it'll be about 43 cycles from start of hsync to start of visible frame, so...
|
||||
// guesses, until I can find information: 26 cycles blank, 16 sync, 40 blank, 160 pixels
|
||||
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
|
||||
if(_vBlankEnabled) {
|
||||
state = OutputState::Blank;
|
||||
} else {
|
||||
state = OutputState::Pixel;
|
||||
}
|
||||
// update the background chain
|
||||
if(_horizontalTimer >= 64 && _horizontalTimer <= 160+64 && !(_horizontalTimer&3))
|
||||
{
|
||||
_playfieldOutput = _nextPlayfieldOutput;
|
||||
_nextPlayfieldOutput = _playfield[(_horizontalTimer - 64) >> 2];
|
||||
}
|
||||
else if(_horizontalTimer < end_of_colour_burst) state = OutputState::Blank;
|
||||
else if(_horizontalTimer < end_of_sync) state = OutputState::ColourBurst;
|
||||
else if(_horizontalTimer < start_of_sync) state = OutputState::Sync;
|
||||
else state = OutputState::Blank;
|
||||
|
||||
// logic: if vsync is enabled, output the opposite of the automatic hsync output
|
||||
// if vsync is enabled, output the opposite of the automatic hsync output;
|
||||
// also honour the vertical blank flag
|
||||
if(_vSyncEnabled) {
|
||||
state = (state = OutputState::Sync) ? OutputState::Blank : OutputState::Sync;
|
||||
effective_state = (effective_state = OutputState::Sync) ? OutputState::Blank : OutputState::Sync;
|
||||
} else if(_vBlankEnabled && effective_state == OutputState::Pixel) {
|
||||
effective_state = OutputState::Blank;
|
||||
}
|
||||
|
||||
// decide what that means needs to be communicated to the CRT
|
||||
_lastOutputStateDuration++;
|
||||
if(state != _lastOutputState) {
|
||||
if(effective_state != _lastOutputState) {
|
||||
switch(_lastOutputState) {
|
||||
case OutputState::Blank: _crt->output_blank(_lastOutputStateDuration); break;
|
||||
case OutputState::Sync: _crt->output_sync(_lastOutputStateDuration); break;
|
||||
@ -246,35 +374,34 @@ void Machine::output_pixels(unsigned int count)
|
||||
case OutputState::Pixel: _crt->output_data(_lastOutputStateDuration, 1); break;
|
||||
}
|
||||
_lastOutputStateDuration = 0;
|
||||
_lastOutputState = state;
|
||||
_lastOutputState = effective_state;
|
||||
|
||||
if(state == OutputState::Pixel) {
|
||||
if(effective_state == OutputState::Pixel) {
|
||||
_outputBuffer = _crt->allocate_write_area(160);
|
||||
} else {
|
||||
_outputBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
|
||||
uint8_t throwaway_pixel;
|
||||
get_output_pixel(_outputBuffer ? &_outputBuffer[_lastOutputStateDuration] : &throwaway_pixel, 159 - _horizontalTimer);
|
||||
|
||||
// increment all graphics counters
|
||||
increment_object_counter(0);
|
||||
increment_object_counter(1);
|
||||
increment_object_counter(2);
|
||||
increment_object_counter(3);
|
||||
increment_object_counter(4);
|
||||
// decide on a pixel colour if that's what's happening
|
||||
if(effective_state == OutputState::Pixel)
|
||||
{
|
||||
uint8_t colour = get_output_pixel();
|
||||
if(_outputBuffer)
|
||||
{
|
||||
*_outputBuffer = colour;
|
||||
_outputBuffer++;
|
||||
}
|
||||
}
|
||||
|
||||
// assumption here: signed shifts right; otherwise it's just
|
||||
// an attempt to avoid both the % operator and a conditional
|
||||
_horizontalTimer--;
|
||||
const int32_t sign_extension = _horizontalTimer >> 31;
|
||||
_horizontalTimer = (_horizontalTimer&~sign_extension) | (sign_extension&horizontalTimerReload);
|
||||
|
||||
// advance horizontal timer, perform reset actions if desired
|
||||
_horizontalTimer = (_horizontalTimer + 1) % horizontalTimerPeriod;
|
||||
if(!_horizontalTimer)
|
||||
_vBlankExtend = false;
|
||||
{
|
||||
// switch back to a normal length left border
|
||||
_stateByTime = _stateByExtendTime[0];
|
||||
set_ready_line(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,26 +410,19 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
set_reset_line(false);
|
||||
|
||||
uint8_t returnValue = 0xff;
|
||||
unsigned int cycles_run_for = 1;
|
||||
const int32_t ready_line_disable_time = 227;//horizontalTimerReload;
|
||||
unsigned int cycles_run_for = 3;
|
||||
|
||||
// this occurs as a feedback loop — the 2600 requests ready, then performs the cycles_run_for
|
||||
// leap to the end of ready only once ready is signalled — because on a 6502 ready doesn't take
|
||||
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
|
||||
// skips to the end of the line.
|
||||
if(operation == CPU6502::BusOperation::Ready) {
|
||||
unsigned int distance_to_end_of_ready = (_horizontalTimer - ready_line_disable_time + horizontalTimerReload + 1)%(horizontalTimerReload + 1);
|
||||
cycles_run_for = distance_to_end_of_ready / 3;
|
||||
output_pixels(distance_to_end_of_ready);
|
||||
} else {
|
||||
output_pixels(3);
|
||||
unsigned int distance_to_end_of_ready = horizontalTimerPeriod - _horizontalTimer;
|
||||
cycles_run_for = distance_to_end_of_ready;
|
||||
}
|
||||
|
||||
if(_hMoveWillCount) {
|
||||
_hMoveCounter = 0x0f;
|
||||
_hMoveFlags = 0x1f;
|
||||
_hMoveIsCounting = true;
|
||||
_hMoveWillCount = false;
|
||||
}
|
||||
|
||||
if(_horizontalTimer == ready_line_disable_time)
|
||||
set_ready_line(false);
|
||||
output_pixels(cycles_run_for);
|
||||
_cycles_since_speaker_update += cycles_run_for;
|
||||
|
||||
if(operation != CPU6502::BusOperation::Ready) {
|
||||
|
||||
@ -378,14 +498,26 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
case 0x01: _vBlankEnabled = !!(*value & 0x02); break;
|
||||
|
||||
case 0x02:
|
||||
set_ready_line(true);
|
||||
if(_horizontalTimer) set_ready_line(true);
|
||||
break;
|
||||
case 0x03:
|
||||
_horizontalTimer = 0;
|
||||
// Reset is delayed by four cycles.
|
||||
_horizontalTimer = horizontalTimerPeriod - 4;
|
||||
|
||||
// TODO: audio will now be out of synchronisation — fix
|
||||
break;
|
||||
|
||||
case 0x04:
|
||||
case 0x05: _playerAndMissileSize[decodedAddress - 0x04] = *value; break;
|
||||
case 0x05: {
|
||||
int entry = decodedAddress - 0x04;
|
||||
_playerAndMissileSize[entry] = *value;
|
||||
_missileSize[entry] = 1 << ((*value >> 4)&3);
|
||||
|
||||
uint8_t repeatMask = (*value)&7;
|
||||
_hasSecondCopy[entry] = (repeatMask == 1) || (repeatMask == 3);
|
||||
_hasThirdCopy[entry] = (repeatMask == 2) || (repeatMask == 3) || (repeatMask == 6);
|
||||
_hasFourthCopy[entry] = (repeatMask == 4) || (repeatMask == 6);
|
||||
} break;
|
||||
|
||||
case 0x06:
|
||||
case 0x07: _playerColour[decodedAddress - 0x06] = *value; break;
|
||||
@ -395,6 +527,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
case 0x0a: {
|
||||
uint8_t old_playfield_control = _playfieldControl;
|
||||
_playfieldControl = *value;
|
||||
_ballSize = 1 << ((_playfieldControl >> 4)&3);
|
||||
|
||||
// did the mirroring bit change?
|
||||
if((_playfieldControl^old_playfield_control)&1) {
|
||||
@ -406,7 +539,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
}
|
||||
} break;
|
||||
case 0x0b:
|
||||
case 0x0c: _playerReflection[decodedAddress - 0x0b] = *value; break;
|
||||
case 0x0c: _playerReflectionMask[decodedAddress - 0x0b] = (*value)&8 ? 0 : 7; break;
|
||||
|
||||
case 0x0d:
|
||||
_playfield[0] = ((*value) >> 4)&1;
|
||||
@ -454,23 +587,40 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
break;
|
||||
|
||||
case 0x10: case 0x11: case 0x12: case 0x13:
|
||||
case 0x14: _objectCounter[decodedAddress - 0x10] = 0; break;
|
||||
case 0x14:
|
||||
_upcomingEvents[(_upcomingEventsPointer + 4)%number_of_upcoming_events].updates |= Event::Action::ResetCounter;
|
||||
_upcomingEvents[(_upcomingEventsPointer + 4)%number_of_upcoming_events].counter = decodedAddress - 0x10;
|
||||
break;
|
||||
|
||||
case 0x15: case 0x16:
|
||||
update_audio();
|
||||
_speaker.set_control(decodedAddress - 0x15, *value);
|
||||
break;
|
||||
|
||||
case 0x17: case 0x18:
|
||||
update_audio();
|
||||
_speaker.set_divider(decodedAddress - 0x17, *value);
|
||||
break;
|
||||
|
||||
case 0x19: case 0x1a:
|
||||
update_audio();
|
||||
_speaker.set_volume(decodedAddress - 0x19, *value);
|
||||
break;
|
||||
|
||||
case 0x1c:
|
||||
_ballGraphicsEnable = _ballGraphicsEnableLatch;
|
||||
_ballGraphicsEnable[1] = _ballGraphicsEnable[0];
|
||||
case 0x1b: {
|
||||
int index = decodedAddress - 0x1b;
|
||||
_playerGraphicsLatch[index] = *value;
|
||||
if(!(_playerGraphicsLatchEnable[index]&1))
|
||||
_playerGraphics[index] = _playerGraphicsLatch[index];
|
||||
_playerGraphics[index^1] = _playerGraphicsLatch[index^1];
|
||||
_playerGraphics[0][index] = *value;
|
||||
_playerGraphics[1][index^1] = _playerGraphics[0][index^1];
|
||||
} break;
|
||||
case 0x1d: _missileGraphicsEnable[0] = *value; break;
|
||||
case 0x1e: _missileGraphicsEnable[1] = *value; break;
|
||||
case 0x1d:
|
||||
case 0x1e:
|
||||
_missileGraphicsEnable[decodedAddress - 0x1d] = ((*value) >> 1)&1;
|
||||
// printf("e:%02x <- %c\n", decodedAddress - 0x1d, ((*value)&1) ? 'E' : '-');
|
||||
break;
|
||||
case 0x1f:
|
||||
_ballGraphicsEnableLatch = *value;
|
||||
if(!(_ballGraphicsEnableDelay&1))
|
||||
_ballGraphicsEnable = _ballGraphicsEnableLatch;
|
||||
_ballGraphicsEnable[0] = ((*value) >> 1)&1;
|
||||
break;
|
||||
|
||||
case 0x20:
|
||||
@ -481,21 +631,42 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
_objectMotion[decodedAddress - 0x20] = *value;
|
||||
break;
|
||||
|
||||
case 0x25: _playerGraphicsLatchEnable[0] = *value; break;
|
||||
case 0x26: _playerGraphicsLatchEnable[1] = *value; break;
|
||||
case 0x27: _ballGraphicsEnableDelay = *value; break;
|
||||
case 0x25: _playerGraphicsSelector[0] = (*value)&1; break;
|
||||
case 0x26: _playerGraphicsSelector[1] = (*value)&1; break;
|
||||
case 0x27: _ballGraphicsSelector = (*value)&1; break;
|
||||
|
||||
case 0x28:
|
||||
case 0x29:
|
||||
if(!(*value&0x02) && _missileGraphicsReset[decodedAddress - 0x28]&0x02)
|
||||
_objectCounter[decodedAddress - 0x26] = _objectCounter[decodedAddress - 0x28]; // TODO: +3 for normal, +6 for double, +10 for quad
|
||||
_missileGraphicsReset[decodedAddress - 0x28] = *value;
|
||||
{
|
||||
// TODO: this should properly mean setting a flag and propagating later, I think?
|
||||
int index = decodedAddress - 0x28;
|
||||
if(!(*value&0x02) && _missileGraphicsReset[index])
|
||||
{
|
||||
_objectCounter[_objectCounterPointer][index + 2].count = _objectCounter[_objectCounterPointer][index].count;
|
||||
|
||||
uint8_t repeatMask = _playerAndMissileSize[index] & 7;
|
||||
int extra_offset;
|
||||
switch(repeatMask)
|
||||
{
|
||||
default: extra_offset = 3; break;
|
||||
case 5: extra_offset = 6; break;
|
||||
case 7: extra_offset = 10; break;
|
||||
}
|
||||
|
||||
_objectCounter[_objectCounterPointer][index + 2].count = (_objectCounter[_objectCounterPointer][index + 2].count + extra_offset)%160;
|
||||
}
|
||||
_missileGraphicsReset[index] = !!((*value) & 0x02);
|
||||
// printf("r:%02x <- %c\n", decodedAddress - 0x28, ((*value)&2) ? 'R' : '-');
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x2a:
|
||||
_vBlankExtend = true;
|
||||
_hMoveWillCount = true;
|
||||
break;
|
||||
case 0x2a: {
|
||||
// justification for +5: "we need to wait at least 71 [clocks] before the HMOVE operation is complete";
|
||||
// which will take 16*4 + 2 = 66 cycles from the first compare, implying the first compare must be
|
||||
// in five cycles from now
|
||||
// int start_pause = ((_horizontalTimer + 3)&3) + 4;
|
||||
_upcomingEvents[(_upcomingEventsPointer + 5)%number_of_upcoming_events].updates |= Event::Action::HMoveSetup;
|
||||
} break;
|
||||
case 0x2b:
|
||||
_objectMotion[0] =
|
||||
_objectMotion[1] =
|
||||
@ -525,8 +696,10 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
case 0x01:
|
||||
case 0x03:
|
||||
// TODO: port DDR
|
||||
printf("!!!DDR!!!");
|
||||
break;
|
||||
case 0x04:
|
||||
case 0x06:
|
||||
returnValue &= _piaTimerValue >> _piaTimerShift;
|
||||
|
||||
if(_writtenPiaTimerShift != _piaTimerShift) {
|
||||
@ -535,8 +708,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
}
|
||||
break;
|
||||
case 0x05:
|
||||
case 0x07:
|
||||
returnValue &= _piaTimerStatus;
|
||||
_piaTimerStatus &= ~0x40;
|
||||
_piaTimerStatus &= ~0x80;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@ -546,9 +720,9 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
case 0x05:
|
||||
case 0x06:
|
||||
case 0x07:
|
||||
_writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07);
|
||||
_piaTimerValue = (unsigned int)(*value << _piaTimerShift);
|
||||
_piaTimerStatus &= ~0xc0;
|
||||
_writtenPiaTimerShift = _piaTimerShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07); // i.e. 0, 3, 6, 10
|
||||
_piaTimerValue = (unsigned int)(*value) << _piaTimerShift;
|
||||
_piaTimerStatus &= ~0x40;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -559,15 +733,26 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
}
|
||||
}
|
||||
|
||||
if(_piaTimerValue >= cycles_run_for) {
|
||||
_piaTimerValue -= cycles_run_for;
|
||||
if(_piaTimerValue >= cycles_run_for / 3) {
|
||||
_piaTimerValue -= cycles_run_for / 3;
|
||||
} else {
|
||||
_piaTimerValue += 0xff - cycles_run_for;
|
||||
_piaTimerValue = 0x100 + ((_piaTimerValue - (cycles_run_for / 3)) >> _piaTimerShift);
|
||||
_piaTimerShift = 0;
|
||||
_piaTimerStatus |= 0xc0;
|
||||
}
|
||||
|
||||
return cycles_run_for;
|
||||
// static unsigned int total_cycles = 0;
|
||||
// total_cycles += cycles_run_for / 3;
|
||||
// static time_t logged_time = 0;
|
||||
// time_t time_now = time(nullptr);
|
||||
// if(time_now - logged_time > 0)
|
||||
// {
|
||||
// printf("[c] %ld : %d\n", time_now - logged_time, total_cycles);
|
||||
// total_cycles = 0;
|
||||
// logged_time = time_now;
|
||||
// }
|
||||
|
||||
return cycles_run_for / 3;
|
||||
}
|
||||
|
||||
void Machine::set_digital_input(Atari2600DigitalInput input, bool state)
|
||||
@ -615,3 +800,159 @@ void Machine::set_rom(size_t length, const uint8_t *data)
|
||||
_romPages[2] = &_rom[2048 & romMask];
|
||||
_romPages[3] = &_rom[3072 & romMask];
|
||||
}
|
||||
|
||||
#pragma mark - Audio
|
||||
|
||||
void Machine::update_audio()
|
||||
{
|
||||
unsigned int audio_cycles = _cycles_since_speaker_update / 114;
|
||||
|
||||
// static unsigned int total_cycles = 0;
|
||||
// total_cycles += audio_cycles;
|
||||
// static time_t logged_time = 0;
|
||||
// time_t time_now = time(nullptr);
|
||||
// if(time_now - logged_time > 0)
|
||||
// {
|
||||
// printf("[s] %ld : %d\n", time_now - logged_time, total_cycles);
|
||||
// total_cycles = 0;
|
||||
// logged_time = time_now;
|
||||
// }
|
||||
|
||||
_speaker.run_for_cycles(audio_cycles);
|
||||
_cycles_since_speaker_update %= 114;
|
||||
}
|
||||
|
||||
void Machine::synchronise()
|
||||
{
|
||||
update_audio();
|
||||
}
|
||||
|
||||
Atari2600::Speaker::Speaker()
|
||||
{
|
||||
_poly4_counter[0] = _poly4_counter[1] = 0x00f;
|
||||
_poly5_counter[0] = _poly5_counter[1] = 0x01f;
|
||||
_poly9_counter[0] = _poly9_counter[1] = 0x1ff;
|
||||
}
|
||||
|
||||
Atari2600::Speaker::~Speaker()
|
||||
{
|
||||
}
|
||||
|
||||
void Atari2600::Speaker::set_volume(int channel, uint8_t volume)
|
||||
{
|
||||
_volume[channel] = volume & 0xf;
|
||||
}
|
||||
|
||||
void Atari2600::Speaker::set_divider(int channel, uint8_t divider)
|
||||
{
|
||||
_divider[channel] = divider & 0x1f;
|
||||
_divider_counter[channel] = 0;
|
||||
}
|
||||
|
||||
void Atari2600::Speaker::set_control(int channel, uint8_t control)
|
||||
{
|
||||
_control[channel] = control & 0xf;
|
||||
}
|
||||
|
||||
#define advance_poly4(c) _poly4_counter[channel] = (_poly4_counter[channel] >> 1) | (((_poly4_counter[channel] << 3) ^ (_poly4_counter[channel] << 2))&0x008)
|
||||
#define advance_poly5(c) _poly5_counter[channel] = (_poly5_counter[channel] >> 1) | (((_poly5_counter[channel] << 4) ^ (_poly5_counter[channel] << 2))&0x010)
|
||||
#define advance_poly9(c) _poly9_counter[channel] = (_poly9_counter[channel] >> 1) | (((_poly9_counter[channel] << 4) ^ (_poly9_counter[channel] << 8))&0x100)
|
||||
|
||||
|
||||
void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
|
||||
{
|
||||
for(unsigned int c = 0; c < number_of_samples; c++)
|
||||
{
|
||||
target[c] = 0;
|
||||
for(int channel = 0; channel < 2; channel++)
|
||||
{
|
||||
_divider_counter[channel] ++;
|
||||
int level = 0;
|
||||
switch(_control[channel])
|
||||
{
|
||||
case 0x0: case 0xb: // constant 1
|
||||
level = 1;
|
||||
break;
|
||||
|
||||
case 0x4: case 0x5: // div2 tone
|
||||
level = (_divider_counter[channel] / (_divider[channel]+1))&1;
|
||||
break;
|
||||
|
||||
case 0xc: case 0xd: // div6 tone
|
||||
level = (_divider_counter[channel] / ((_divider[channel]+1)*3))&1;
|
||||
break;
|
||||
|
||||
case 0x6: case 0xa: // div31 tone
|
||||
level = (_divider_counter[channel] / (_divider[channel]+1))%30 <= 18;
|
||||
break;
|
||||
|
||||
case 0xe: // div93 tone
|
||||
level = (_divider_counter[channel] / ((_divider[channel]+1)*3))%30 <= 18;
|
||||
break;
|
||||
|
||||
case 0x1: // 4-bit poly
|
||||
level = _poly4_counter[channel]&1;
|
||||
if(_divider_counter[channel] == _divider[channel]+1)
|
||||
{
|
||||
_divider_counter[channel] = 0;
|
||||
advance_poly4(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x2: // 4-bit poly div31
|
||||
level = _poly4_counter[channel]&1;
|
||||
if(_divider_counter[channel]%(30*(_divider[channel]+1)) == 18)
|
||||
{
|
||||
advance_poly4(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x3: // 5/4-bit poly
|
||||
level = _output_state[channel];
|
||||
if(_divider_counter[channel] == _divider[channel]+1)
|
||||
{
|
||||
if(_poly5_counter[channel]&1)
|
||||
{
|
||||
_output_state[channel] = _poly4_counter[channel]&1;
|
||||
advance_poly4(channel);
|
||||
}
|
||||
advance_poly5(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x7: case 0x9: // 5-bit poly
|
||||
level = _poly5_counter[channel]&1;
|
||||
if(_divider_counter[channel] == _divider[channel]+1)
|
||||
{
|
||||
_divider_counter[channel] = 0;
|
||||
advance_poly5(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf: // 5-bit poly div6
|
||||
level = _poly5_counter[channel]&1;
|
||||
if(_divider_counter[channel] == (_divider[channel]+1)*3)
|
||||
{
|
||||
_divider_counter[channel] = 0;
|
||||
advance_poly5(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x8: // 9-bit poly
|
||||
level = _poly9_counter[channel]&1;
|
||||
if(_divider_counter[channel] == _divider[channel]+1)
|
||||
{
|
||||
_divider_counter[channel] = 0;
|
||||
advance_poly9(channel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
target[c] += _volume[channel] * 1024 * level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Atari2600::Speaker::skip_samples(unsigned int number_of_samples)
|
||||
{
|
||||
}
|
||||
|
@ -10,28 +10,64 @@
|
||||
#define Atari2600_cpp
|
||||
|
||||
#include "../../Processors/6502/CPU6502.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../CRTMachine.hpp"
|
||||
#include <stdint.h>
|
||||
#include "Atari2600Inputs.h"
|
||||
|
||||
namespace Atari2600 {
|
||||
|
||||
class Machine: public CPU6502::Processor<Machine> {
|
||||
const unsigned int number_of_upcoming_events = 6;
|
||||
const unsigned int number_of_recorded_counters = 7;
|
||||
|
||||
class Speaker: public ::Outputs::Filter<Speaker> {
|
||||
public:
|
||||
Speaker();
|
||||
~Speaker();
|
||||
|
||||
void set_volume(int channel, uint8_t volume);
|
||||
void set_divider(int channel, uint8_t divider);
|
||||
void set_control(int channel, uint8_t control);
|
||||
|
||||
void get_samples(unsigned int number_of_samples, int16_t *target);
|
||||
void skip_samples(unsigned int number_of_samples);
|
||||
|
||||
private:
|
||||
uint8_t _volume[2];
|
||||
uint8_t _divider[2];
|
||||
uint8_t _control[2];
|
||||
|
||||
int _poly4_counter[2];
|
||||
int _poly5_counter[2];
|
||||
int _poly9_counter[2];
|
||||
int _output_state[2];
|
||||
|
||||
int _divider_counter[2];
|
||||
|
||||
int _pattern_periods[16];
|
||||
int _patterns[16][512];
|
||||
};
|
||||
|
||||
class Machine: public CPU6502::Processor<Machine>, public CRTMachine::Machine {
|
||||
|
||||
public:
|
||||
Machine();
|
||||
~Machine();
|
||||
|
||||
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
|
||||
void set_rom(size_t length, const uint8_t *data);
|
||||
void switch_region();
|
||||
|
||||
void set_digital_input(Atari2600DigitalInput input, bool state);
|
||||
|
||||
Outputs::CRT::CRT *get_crt() { return _crt; }
|
||||
void setup_output(float aspect_ratio);
|
||||
void close_output();
|
||||
// to satisfy CPU6502::Processor
|
||||
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
void synchronise();
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
virtual void setup_output(float aspect_ratio);
|
||||
virtual void close_output();
|
||||
virtual Outputs::CRT::CRT *get_crt() { return _crt; }
|
||||
virtual Outputs::Speaker *get_speaker() { return &_speaker; }
|
||||
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
|
||||
|
||||
private:
|
||||
uint8_t *_rom, *_romPages[4], _ram[128];
|
||||
@ -46,32 +82,81 @@ class Machine: public CPU6502::Processor<Machine> {
|
||||
uint8_t _playfieldControl;
|
||||
uint8_t _playfieldColour;
|
||||
uint8_t _backgroundColour;
|
||||
uint8_t _playfield[40];
|
||||
uint8_t _playfield[41];
|
||||
|
||||
// ... and derivatives
|
||||
int _ballSize, _missileSize[2];
|
||||
|
||||
// delayed clock events
|
||||
enum OutputState {
|
||||
Sync,
|
||||
Blank,
|
||||
ColourBurst,
|
||||
Pixel
|
||||
};
|
||||
|
||||
struct Event {
|
||||
enum Action {
|
||||
Playfield = 1 << 0,
|
||||
ResetCounter = 1 << 1,
|
||||
|
||||
HMoveSetup = 1 << 2,
|
||||
HMoveCompare = 1 << 3,
|
||||
HMoveDecrement = 1 << 4,
|
||||
};
|
||||
int updates;
|
||||
|
||||
OutputState state;
|
||||
uint8_t playfieldPixel;
|
||||
int counter;
|
||||
|
||||
Event() : updates(0), playfieldPixel(0) {}
|
||||
} _upcomingEvents[number_of_upcoming_events];
|
||||
unsigned int _upcomingEventsPointer;
|
||||
|
||||
// object counters
|
||||
struct ObjectCounter {
|
||||
int count; // the counter value, multiplied by four, counting phase
|
||||
int pixel; // for non-sprite objects, a count of cycles since the last counter reset; for sprite objects a count of pixels so far elapsed
|
||||
int broad_pixel; // for sprite objects, a count of cycles since the last counter reset; otherwise unused
|
||||
|
||||
ObjectCounter() : count(0), pixel(0), broad_pixel(0) {}
|
||||
} _objectCounter[number_of_recorded_counters][5];
|
||||
unsigned int _objectCounterPointer;
|
||||
|
||||
// the latched playfield output
|
||||
uint8_t _playfieldOutput, _nextPlayfieldOutput;
|
||||
|
||||
// player registers
|
||||
uint8_t _playerColour[2];
|
||||
uint8_t _playerReflection[2];
|
||||
uint8_t _playerGraphicsLatch[2], _playerGraphics[2];
|
||||
uint8_t _playerGraphicsLatchEnable[2];
|
||||
uint8_t _playerReflectionMask[2];
|
||||
uint8_t _playerGraphics[2][2];
|
||||
uint8_t _playerGraphicsSelector[2];
|
||||
bool _playerStart[2];
|
||||
|
||||
// object flags
|
||||
bool _hasSecondCopy[2];
|
||||
bool _hasThirdCopy[2];
|
||||
bool _hasFourthCopy[2];
|
||||
uint8_t _objectMotion[5]; // the value stored to this counter's motion register
|
||||
|
||||
// player + missile registers
|
||||
uint8_t _playerAndMissileSize[2];
|
||||
|
||||
// missile registers
|
||||
uint8_t _missileGraphicsEnable[2], _missileGraphicsReset[2];
|
||||
uint8_t _missileGraphicsEnable[2];
|
||||
bool _missileGraphicsReset[2];
|
||||
|
||||
// ball registers
|
||||
uint8_t _ballGraphicsEnable, _ballGraphicsEnableLatch;
|
||||
uint8_t _ballGraphicsEnableDelay;
|
||||
uint8_t _ballGraphicsEnable[2];
|
||||
uint8_t _ballGraphicsSelector;
|
||||
|
||||
// graphics output
|
||||
int32_t _horizontalTimer;
|
||||
unsigned int _horizontalTimer;
|
||||
bool _vSyncEnabled, _vBlankEnabled;
|
||||
bool _vBlankExtend;
|
||||
|
||||
// horizontal motion control
|
||||
uint8_t _hMoveCounter;
|
||||
bool _hMoveIsCounting, _hMoveWillCount;
|
||||
uint8_t _objectCounter[5], _objectMotion[5];
|
||||
uint8_t _hMoveFlags;
|
||||
|
||||
// joystick state
|
||||
@ -82,21 +167,28 @@ class Machine: public CPU6502::Processor<Machine> {
|
||||
// collisions
|
||||
uint8_t _collisions[8];
|
||||
|
||||
enum OutputState {
|
||||
Sync,
|
||||
Blank,
|
||||
ColourBurst,
|
||||
Pixel
|
||||
};
|
||||
|
||||
void output_pixels(unsigned int count);
|
||||
void get_output_pixel(uint8_t *pixel, int offset);
|
||||
uint8_t get_output_pixel();
|
||||
void update_timers(int mask);
|
||||
|
||||
// outputs
|
||||
Outputs::CRT::CRT *_crt;
|
||||
Speaker _speaker;
|
||||
|
||||
// speaker backlog accumlation counter
|
||||
unsigned int _cycles_since_speaker_update;
|
||||
void update_audio();
|
||||
|
||||
// latched output state
|
||||
unsigned int _lastOutputStateDuration;
|
||||
OutputState _stateByExtendTime[2][57];
|
||||
OutputState *_stateByTime;
|
||||
OutputState _lastOutputState;
|
||||
uint8_t *_outputBuffer;
|
||||
|
||||
// lookup table for collision reporting
|
||||
uint8_t _reportedCollisions[64][8];
|
||||
void setup_reported_collisions();
|
||||
};
|
||||
|
||||
}
|
||||
|
30
Machines/CRTMachine.hpp
Normal file
30
Machines/CRTMachine.hpp
Normal 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 */
|
@ -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();
|
||||
|
@ -10,9 +10,8 @@
|
||||
#define Electron_hpp
|
||||
|
||||
#include "../../Processors/6502/CPU6502.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../Outputs/Speaker.hpp"
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../CRTMachine.hpp"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace Electron {
|
||||
@ -142,30 +141,34 @@ class Speaker: public ::Outputs::Filter<Speaker> {
|
||||
@discussion An instance of Electron::Machine represents the current state of an
|
||||
Acorn Electron.
|
||||
*/
|
||||
class Machine: public CPU6502::Processor<Machine>, Tape::Delegate {
|
||||
class Machine: public CPU6502::Processor<Machine>, public CRTMachine::Machine, Tape::Delegate {
|
||||
|
||||
public:
|
||||
|
||||
Machine();
|
||||
|
||||
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
|
||||
void set_rom(ROMSlot slot, size_t length, const uint8_t *data);
|
||||
void set_tape(std::shared_ptr<Storage::Tape> tape);
|
||||
|
||||
void set_key_state(Key key, bool isPressed);
|
||||
void clear_all_keys();
|
||||
|
||||
void setup_output(float aspect_ratio);
|
||||
void close_output();
|
||||
Outputs::CRT::CRT *get_crt() { return _crt.get(); }
|
||||
Outputs::Speaker *get_speaker() { return &_speaker; }
|
||||
|
||||
virtual void tape_did_change_interrupt_status(Tape *tape);
|
||||
|
||||
void update_output();
|
||||
inline void set_use_fast_tape_hack(bool activate) { _use_fast_tape_hack = activate; }
|
||||
|
||||
// to satisfy CPU6502::Processor
|
||||
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
void synchronise();
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
virtual void setup_output(float aspect_ratio);
|
||||
virtual void close_output();
|
||||
virtual Outputs::CRT::CRT *get_crt() { return _crt.get(); }
|
||||
virtual Outputs::Speaker *get_speaker() { return &_speaker; }
|
||||
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
|
||||
|
||||
// to satisfy Tape::Delegate
|
||||
virtual void tape_did_change_interrupt_status(Tape *tape);
|
||||
|
||||
private:
|
||||
|
||||
inline void update_display();
|
||||
@ -215,12 +218,12 @@ class Machine: public CPU6502::Processor<Machine>, Tape::Delegate {
|
||||
uint8_t *_current_output_target, *_initial_output_target;
|
||||
unsigned int _current_output_divider;
|
||||
|
||||
// Tape.
|
||||
// Tape
|
||||
Tape _tape;
|
||||
bool _use_fast_tape_hack;
|
||||
bool _fast_load_is_in_data;
|
||||
|
||||
// Outputs.
|
||||
// Outputs
|
||||
std::unique_ptr<Outputs::CRT::CRT> _crt;
|
||||
Speaker _speaker;
|
||||
};
|
||||
|
@ -334,6 +334,7 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
4B046DC31CFE651500E9E45E /* CRTMachine.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTMachine.hpp; sourceTree = "<group>"; };
|
||||
4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTConstants.hpp; sourceTree = "<group>"; };
|
||||
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRT.cpp; sourceTree = "<group>"; };
|
||||
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRT.hpp; sourceTree = "<group>"; };
|
||||
@ -1192,6 +1193,7 @@
|
||||
children = (
|
||||
4B2E2D961C3A06EC00138695 /* Atari2600 */,
|
||||
4B2E2D9E1C3A070900138695 /* Electron */,
|
||||
4B046DC31CFE651500E9E45E /* CRTMachine.hpp */,
|
||||
);
|
||||
name = Machines;
|
||||
path = ../../Machines;
|
||||
|
@ -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? {
|
||||
|
@ -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()
|
||||
|
@ -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) {}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -15,6 +15,4 @@
|
||||
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput;
|
||||
- (void)setResetLineEnabled:(BOOL)enabled;
|
||||
|
||||
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -58,6 +58,7 @@ class OpenGLOutputBuilder {
|
||||
// the run and input data buffers
|
||||
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
|
||||
std::unique_ptr<std::mutex> _output_mutex;
|
||||
std::unique_ptr<std::mutex> _draw_mutex;
|
||||
|
||||
// transient buffers indicating composite data not yet decoded
|
||||
GLsizei _composite_src_output_y, _cleared_composite_output_y;
|
||||
|
@ -269,7 +269,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shade
|
||||
"), vec3(1.0))"
|
||||
");"
|
||||
|
||||
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 4.0, 4.0);"
|
||||
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 2.0, 2.0);"
|
||||
"fragColour = lumaChromaToRGB * lumaChromaColourInRange;"
|
||||
"}", false, false);
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
|
||||
|
||||
"void main(void)"
|
||||
"{"
|
||||
"fragColour = vec4(%s, 0.5*cos(lateralVarying));"
|
||||
"fragColour = vec4(%s, 0.5);"//*cos(lateralVarying)
|
||||
"}",
|
||||
sampler_type, fragment_methods, colour_expression);
|
||||
|
||||
|
@ -24,6 +24,18 @@ class Speaker {
|
||||
virtual void speaker_did_complete_samples(Speaker *speaker, const int16_t *buffer, int buffer_size) = 0;
|
||||
};
|
||||
|
||||
int get_ideal_clock_rate_in_range(int minimum, int maximum)
|
||||
{
|
||||
// return exactly the input rate if possible
|
||||
if(_input_cycles_per_second >= minimum && _input_cycles_per_second <= maximum) return _input_cycles_per_second;
|
||||
|
||||
// if the input rate is lower, return the minimum
|
||||
if(_input_cycles_per_second < minimum) return minimum;
|
||||
|
||||
// otherwise, return the maximum
|
||||
return maximum;
|
||||
}
|
||||
|
||||
void set_output_rate(int cycles_per_second, int buffer_size)
|
||||
{
|
||||
_output_cycles_per_second = cycles_per_second;
|
||||
@ -76,20 +88,16 @@ template <class T> class Filter: public Speaker {
|
||||
{
|
||||
if(_coefficients_are_dirty) update_filter_coefficients();
|
||||
|
||||
// TODO: what if output rate is greater than input rate?
|
||||
|
||||
// fill up as much of the input buffer as possible
|
||||
while(input_cycles)
|
||||
// if input and output rates exactly match, just accumulate results and pass on
|
||||
if(_input_cycles_per_second == _output_cycles_per_second)
|
||||
{
|
||||
unsigned int cycles_to_read = (unsigned int)std::min((int)input_cycles, _number_of_taps - _input_buffer_depth);
|
||||
static_cast<T *>(this)->get_samples(cycles_to_read, &_input_buffer.get()[_input_buffer_depth]);
|
||||
input_cycles -= cycles_to_read;
|
||||
_input_buffer_depth += cycles_to_read;
|
||||
|
||||
if(_input_buffer_depth == _number_of_taps)
|
||||
while(input_cycles)
|
||||
{
|
||||
_buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get());
|
||||
_buffer_in_progress_pointer++;
|
||||
unsigned int cycles_to_read = (unsigned int)(_buffer_size - _buffer_in_progress_pointer);
|
||||
if(cycles_to_read > input_cycles) cycles_to_read = input_cycles;
|
||||
|
||||
static_cast<T *>(this)->get_samples(cycles_to_read, &_buffer_in_progress.get()[_buffer_in_progress_pointer]);
|
||||
_buffer_in_progress_pointer += cycles_to_read;
|
||||
|
||||
// announce to delegate if full
|
||||
if(_buffer_in_progress_pointer == _buffer_size)
|
||||
@ -101,24 +109,60 @@ template <class T> class Filter: public Speaker {
|
||||
}
|
||||
}
|
||||
|
||||
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
|
||||
// preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip
|
||||
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
|
||||
uint64_t steps = _stepper->step();
|
||||
if(steps < _number_of_taps)
|
||||
input_cycles -= cycles_to_read;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if the output rate is less than the input rate, use the filter
|
||||
if(_input_cycles_per_second > _output_cycles_per_second)
|
||||
{
|
||||
while(input_cycles)
|
||||
{
|
||||
unsigned int cycles_to_read = (unsigned int)std::min((int)input_cycles, _number_of_taps - _input_buffer_depth);
|
||||
static_cast<T *>(this)->get_samples(cycles_to_read, &_input_buffer.get()[_input_buffer_depth]);
|
||||
input_cycles -= cycles_to_read;
|
||||
_input_buffer_depth += cycles_to_read;
|
||||
|
||||
if(_input_buffer_depth == _number_of_taps)
|
||||
{
|
||||
int16_t *input_buffer = _input_buffer.get();
|
||||
memmove(input_buffer, &input_buffer[steps], sizeof(int16_t) * ((size_t)_number_of_taps - (size_t)steps));
|
||||
_input_buffer_depth -= steps;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(steps > _number_of_taps)
|
||||
static_cast<T *>(this)->skip_samples((unsigned int)steps - (unsigned int)_number_of_taps);
|
||||
_input_buffer_depth = 0;
|
||||
_buffer_in_progress.get()[_buffer_in_progress_pointer] = _filter->apply(_input_buffer.get());
|
||||
_buffer_in_progress_pointer++;
|
||||
|
||||
// announce to delegate if full
|
||||
if(_buffer_in_progress_pointer == _buffer_size)
|
||||
{
|
||||
_buffer_in_progress_pointer = 0;
|
||||
if(_delegate)
|
||||
{
|
||||
_delegate->speaker_did_complete_samples(this, _buffer_in_progress.get(), _buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
|
||||
// preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip
|
||||
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
|
||||
uint64_t steps = _stepper->step();
|
||||
if(steps < _number_of_taps)
|
||||
{
|
||||
int16_t *input_buffer = _input_buffer.get();
|
||||
memmove(input_buffer, &input_buffer[steps], sizeof(int16_t) * ((size_t)_number_of_taps - (size_t)steps));
|
||||
_input_buffer_depth -= steps;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(steps > _number_of_taps)
|
||||
static_cast<T *>(this)->skip_samples((unsigned int)steps - (unsigned int)_number_of_taps);
|
||||
_input_buffer_depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: input rate is less than output rate
|
||||
}
|
||||
|
||||
private:
|
||||
|
@ -67,9 +67,12 @@ extern const uint8_t JamOpcode;
|
||||
@abstact An abstract base class for emulation of a 6502 processor via the curiously recurring template pattern/f-bounded polymorphism.
|
||||
|
||||
@discussion Subclasses should implement @c perform_bus_operation(BusOperation operation, uint16_t address, uint8_t *value) in
|
||||
order to provde the bus on which the 6502 operates. Additional functionality can be provided by the host machine by providing
|
||||
a jam handler and inserting jam opcodes where appropriate; that will cause call outs when the program counter reaches those
|
||||
addresses. @c return_from_subroutine can be used to exit from a jammed state.
|
||||
order to provide the bus on which the 6502 operates and @c synchronise(), which is called upon completion of a continuous run
|
||||
of cycles to allow a subclass to bring any on-demand activities up to date.
|
||||
|
||||
Additional functionality can be provided by the host machine by providing a jam handler and inserting jam opcodes where appropriate;
|
||||
that will cause call outs when the program counter reaches those addresses. @c return_from_subroutine can be used to exit from a
|
||||
jammed state.
|
||||
*/
|
||||
template <class T> class Processor {
|
||||
public:
|
||||
@ -597,6 +600,7 @@ template <class T> class Processor {
|
||||
|
||||
case CycleFetchOperation: {
|
||||
_lastOperationPC = _pc;
|
||||
// printf("%04x x:%02x\n", _pc.full, _x);
|
||||
_pc.full++;
|
||||
read_op(_operation, _lastOperationPC.full);
|
||||
|
||||
@ -1040,15 +1044,17 @@ template <class T> class Processor {
|
||||
_ready_is_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
_cycles_left_to_run = number_of_cycles;
|
||||
_scheduleProgramsReadPointer = scheduleProgramsReadPointer;
|
||||
_scheduleProgramProgramCounter = scheduleProgramProgramCounter;
|
||||
_nextAddress = nextAddress;
|
||||
_nextBusOperation = nextBusOperation;
|
||||
_busAddress = busAddress;
|
||||
_busValue = busValue;
|
||||
}
|
||||
|
||||
_cycles_left_to_run = number_of_cycles;
|
||||
_scheduleProgramsReadPointer = scheduleProgramsReadPointer;
|
||||
_scheduleProgramProgramCounter = scheduleProgramProgramCounter;
|
||||
_nextAddress = nextAddress;
|
||||
_nextBusOperation = nextBusOperation;
|
||||
_busAddress = busAddress;
|
||||
_busValue = busValue;
|
||||
|
||||
static_cast<T *>(this)->synchronise();
|
||||
}
|
||||
|
||||
/*!
|
||||
|
Loading…
Reference in New Issue
Block a user