1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-11 04:28:58 +00:00

Merge pull request #1 from TomHarte/Electron

Adds Acorn Electron emulation, significantly improving the CRT and timing in general and adding sound output for the general-use code.
This commit is contained in:
Thomas Harte 2016-04-24 22:33:20 -04:00
commit 6d769b3639
62 changed files with 6951 additions and 1734 deletions

3
.gitignore vendored
View File

@ -18,6 +18,9 @@ DerivedData
*.xcuserstate
.DS_Store
# Exclude Electron ROMs
OSBindings/Mac/Clock Signal/Resources/Electron/*
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However

View File

@ -24,21 +24,39 @@ Machine::Machine() :
_piaDataValue{0xff, 0xff},
_tiaInputValue{0xff, 0xff}
{
_crt = new Outputs::CRT(228, 262, 1, 2);
memset(_collisions, 0xff, sizeof(_collisions));
setup6502();
set_reset_line(true);
}
void Machine::setup_output(float aspect_ratio)
{
_crt = new Outputs::CRT::CRT(228, 1, 263, Outputs::CRT::ColourSpace::YIQ, 228, 1, 2);
_crt->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)\n"
"{\n"
"vec2 c = vec2(texture(texID, coordinate).rg) / vec2(255.0);"
"float y = 0.1 + c.x * 0.91071428571429;"
"float aOffset = 6.283185308 * (2.0/16.0 - c.y);" // - 3.0 / 16.0
"return y + step(0.03125, c.y) * amplitude * cos(phase - aOffset);"
"}");
_crt->set_output_device(Outputs::CRT::Television);
}
void Machine::close_output()
{
delete _crt;
_crt = nullptr;
}
Machine::~Machine()
{
delete[] _rom;
delete _crt;
close_output();
}
void Machine::switch_region()
{
_crt->set_new_timing(228, 312);
_crt->set_new_timing(228, 312, Outputs::CRT::ColourSpace::YUV, 228, 1);
}
void Machine::get_output_pixel(uint8_t *pixel, int offset)
@ -52,8 +70,7 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
for(int c = 0; c < 2; c++)
{
const uint8_t repeatMask = _playerAndMissileSize[c]&7;
if(_playerGraphics[c])
{
if(_playerGraphics[c]) {
// figure out player colour
int flipMask = (_playerReflection[c]&0x8) ? 0 : 7;
@ -62,9 +79,9 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
{
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;
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;
@ -79,16 +96,15 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
}
// figure out missile colour
if((_missileGraphicsEnable[c]&2) && !(_missileGraphicsReset[c]&2))
{
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;
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;
@ -104,16 +120,14 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
// get the ball proposed colour
uint8_t ballPixel = 0;
if(_ballGraphicsEnable&2)
{
if(_ballGraphicsEnable&2) {
int ballIndex = _objectCounter[4] - 4;
int ballSize = 1 << ((_playfieldControl >> 4)&3);
ballPixel = (ballIndex >= 0 && ballIndex < ballSize) ? 1 : 0;
}
// accumulate collisions
if(playerPixels[0] | playerPixels[1])
{
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);
@ -123,8 +137,7 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
_collisions[7] |= ((playerPixels[0] & playerPixels[1]) << 7);
}
if(playfieldPixel | ballPixel)
{
if(playfieldPixel | ballPixel) {
_collisions[4] |= ((playfieldPixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
_collisions[5] |= ((playfieldPixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
@ -139,8 +152,8 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
uint8_t outputColour = playfieldPixel ? playfieldColour : _backgroundColour;
if(!(_playfieldControl&0x04) || !playfieldPixel) {
if (playerPixels[1] || missilePixels[1]) outputColour = _playerColour[1];
if (playerPixels[0] || missilePixels[0]) outputColour = _playerColour[0];
if(playerPixels[1] || missilePixels[1]) outputColour = _playerColour[1];
if(playerPixels[0] || missilePixels[0]) outputColour = _playerColour[0];
}
// map that colour to separate Y and phase components
@ -148,19 +161,6 @@ void Machine::get_output_pixel(uint8_t *pixel, int offset)
pixel[1] = outputColour&0xf0;
}
const char *Machine::get_signal_decoder()
{
return
"float sample(vec2 coordinate, float phase)\n"
"{\n"
"vec2 c = texture(texID, coordinate).rg;"
"float y = 0.1 + c.x * 0.91071428571429;\n"
"float aOffset = 6.283185308 * (c.y - 3.0 / 16.0) * 1.14285714285714;\n"
"return y + step(0.03125, c.y) * 0.1 * cos(phase - aOffset);\n"
"}";
}
// 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)
@ -175,13 +175,13 @@ void Machine::output_pixels(unsigned int count)
OutputState state;
// update hmove
if (!(_horizontalTimer&3)) {
if(!(_horizontalTimer&3)) {
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);
if(counterValue == (_objectMotion[c] >> 4)) _hMoveFlags &= ~(1 << c);
if(_hMoveFlags&(1 << c)) increment_object_counter(c);
}
}
@ -201,15 +201,15 @@ void Machine::output_pixels(unsigned int count)
// 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(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
if(_vBlankEnabled) {
state = OutputState::Blank;
} else {
state = OutputState::Pixel;
}
}
else if (_horizontalTimer < end_of_sync) state = OutputState::Blank;
else if (_horizontalTimer < start_of_sync) state = OutputState::Sync;
else if(_horizontalTimer < end_of_sync) state = OutputState::Blank;
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
@ -218,28 +218,23 @@ void Machine::output_pixels(unsigned int count)
}
_lastOutputStateDuration++;
if(state != _lastOutputState)
{
switch(_lastOutputState)
{
if(state != _lastOutputState) {
switch(_lastOutputState) {
case OutputState::Blank: _crt->output_blank(_lastOutputStateDuration); break;
case OutputState::Sync: _crt->output_sync(_lastOutputStateDuration); break;
case OutputState::Pixel: _crt->output_data(_lastOutputStateDuration); break;
case OutputState::Pixel: _crt->output_data(_lastOutputStateDuration, 1); break;
}
_lastOutputStateDuration = 0;
_lastOutputState = state;
if(state == OutputState::Pixel)
{
_crt->allocate_write_area(160);
_outputBuffer = _crt->get_write_target_for_buffer(0);
if(state == OutputState::Pixel) {
_outputBuffer = _crt->allocate_write_area(160);
} else {
_outputBuffer = nullptr;
}
}
if(_horizontalTimer < (_vBlankExtend ? 152 : 160))
{
if(_horizontalTimer < (_vBlankExtend ? 152 : 160)) {
if(_outputBuffer)
get_output_pixel(&_outputBuffer[_lastOutputStateDuration << 1], 159 - _horizontalTimer);
@ -314,13 +309,12 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
// check for a ROM read
if ((address&0x1000) && isReadOperation(operation)) {
if((address&0x1000) && isReadOperation(operation)) {
returnValue &= _romPages[(address >> 10)&3][address&1023];
}
// check for a RAM access
if ((address&0x1280) == 0x80) {
if((address&0x1280) == 0x80) {
if(isReadOperation(operation)) {
returnValue &= _ram[address&0x7f];
} else {
@ -329,7 +323,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
// check for a TIA access
if (!(address&0x1080)) {
if(!(address&0x1080)) {
if(isReadOperation(operation)) {
const uint16_t decodedAddress = address & 0xf;
switch(decodedAddress) {
@ -474,7 +468,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
case 0x28:
case 0x29:
if (!(*value&0x02) && _missileGraphicsReset[decodedAddress - 0x28]&0x02)
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;
break;
@ -501,7 +495,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
}
// check for a PIA access
if ((address&0x1280) == 0x280) {
if((address&0x1280) == 0x280) {
if(isReadOperation(operation)) {
const uint8_t decodedAddress = address & 0xf;
switch(address & 0xf) {

View File

@ -9,8 +9,8 @@
#ifndef Atari2600_cpp
#define Atari2600_cpp
#include "../Processors/6502/CPU6502.hpp"
#include "../Outputs/CRT.hpp"
#include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include <stdint.h>
#include "Atari2600Inputs.h"
@ -19,7 +19,6 @@ namespace Atari2600 {
class Machine: public CPU6502::Processor<Machine> {
public:
Machine();
~Machine();
@ -30,9 +29,9 @@ class Machine: public CPU6502::Processor<Machine> {
void set_digital_input(Atari2600DigitalInput input, bool state);
Outputs::CRT *get_crt() { return _crt; }
const char *get_signal_decoder();
Outputs::CRT::CRT *get_crt() { return _crt; }
void setup_output(float aspect_ratio);
void close_output();
private:
uint8_t *_rom, *_romPages[4], _ram[128];
@ -93,7 +92,7 @@ class Machine: public CPU6502::Processor<Machine> {
void output_pixels(unsigned int count);
void get_output_pixel(uint8_t *pixel, int offset);
Outputs::CRT *_crt;
Outputs::CRT::CRT *_crt;
// latched output state
unsigned int _lastOutputStateDuration;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,229 @@
//
// Electron.hpp
// Clock Signal
//
// Created by Thomas Harte on 03/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Electron_hpp
#define Electron_hpp
#include "../../Processors/6502/CPU6502.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../Outputs/Speaker.hpp"
#include "../../Storage/Tape/Tape.hpp"
#include <stdint.h>
namespace Electron {
enum ROMSlot: uint8_t {
ROMSlot0 = 0,
ROMSlot1, ROMSlot2, ROMSlot3,
ROMSlot4, ROMSlot5, ROMSlot6, ROMSlot7,
ROMSlotKeyboard = 8, ROMSlot9,
ROMSlotBASIC = 10, ROMSlot11,
ROMSlot12, ROMSlot13, ROMSlot14, ROMSlot15,
ROMSlotOS
};
enum Interrupt: uint8_t {
PowerOnReset = 0x02,
DisplayEnd = 0x04,
RealTimeClock = 0x08,
ReceiveDataFull = 0x10,
TransmitDataEmpty = 0x20,
HighToneDetect = 0x40
};
enum Key: uint16_t {
KeySpace = 0x0000 | 0x08, KeyCopy = 0x0000 | 0x02, KeyRight = 0x0000 | 0x01,
KeyDelete = 0x0010 | 0x08, KeyReturn = 0x0010 | 0x04, KeyDown = 0x0010 | 0x02, KeyLeft = 0x0010 | 0x01,
KeyColon = 0x0020 | 0x04, KeyUp = 0x0020 | 0x02, KeyMinus = 0x0020 | 0x01,
KeySlash = 0x0030 | 0x08, KeySemiColon = 0x0030 | 0x04, KeyP = 0x0030 | 0x02, Key0 = 0x0030 | 0x01,
KeyFullStop = 0x0040 | 0x08, KeyL = 0x0040 | 0x04, KeyO = 0x0040 | 0x02, Key9 = 0x0040 | 0x01,
KeyComma = 0x0050 | 0x08, KeyK = 0x0050 | 0x04, KeyI = 0x0050 | 0x02, Key8 = 0x0050 | 0x01,
KeyM = 0x0060 | 0x08, KeyJ = 0x0060 | 0x04, KeyU = 0x0060 | 0x02, Key7 = 0x0060 | 0x01,
KeyN = 0x0070 | 0x08, KeyH = 0x0070 | 0x04, KeyY = 0x0070 | 0x02, Key6 = 0x0070 | 0x01,
KeyB = 0x0080 | 0x08, KeyG = 0x0080 | 0x04, KeyT = 0x0080 | 0x02, Key5 = 0x0080 | 0x01,
KeyV = 0x0090 | 0x08, KeyF = 0x0090 | 0x04, KeyR = 0x0090 | 0x02, Key4 = 0x0090 | 0x01,
KeyC = 0x00a0 | 0x08, KeyD = 0x00a0 | 0x04, KeyE = 0x00a0 | 0x02, Key3 = 0x00a0 | 0x01,
KeyX = 0x00b0 | 0x08, KeyS = 0x00b0 | 0x04, KeyW = 0x00b0 | 0x02, Key2 = 0x00b0 | 0x01,
KeyZ = 0x00c0 | 0x08, KeyA = 0x00c0 | 0x04, KeyQ = 0x00c0 | 0x02, Key1 = 0x00c0 | 0x01,
KeyShift = 0x00d0 | 0x08, KeyControl = 0x00d0 | 0x04, KeyFunc = 0x00d0 | 0x02, KeyEscape = 0x00d0 | 0x01,
KeyBreak = 0xffff
};
class Tape {
public:
Tape();
void set_tape(std::shared_ptr<Storage::Tape> tape);
inline bool has_tape()
{
return (bool)_tape;
}
inline uint8_t get_data_register();
inline void set_data_register(uint8_t value);
inline void set_counter(uint8_t value);
inline uint8_t get_interrupt_status() { return _interrupt_status; }
inline void clear_interrupts(uint8_t interrupts);
class Delegate {
public:
virtual void tape_did_change_interrupt_status(Tape *tape) = 0;
};
inline void set_delegate(Delegate *delegate) { _delegate = delegate; }
inline void run_for_cycles(unsigned int number_of_cycles);
inline void run_for_input_pulse();
inline void set_is_running(bool is_running) { _is_running = is_running; }
inline void set_is_enabled(bool is_enabled) { _is_enabled = is_enabled; }
inline void set_is_in_input_mode(bool is_in_input_mode);
private:
inline void push_tape_bit(uint16_t bit);
inline void get_next_tape_pulse();
std::shared_ptr<Storage::Tape> _tape;
struct {
Storage::Tape::Pulse current_pulse;
std::unique_ptr<SignalProcessing::Stepper> pulse_stepper;
uint32_t time_into_pulse;
int minimum_bits_until_full;
} _input;
struct {
unsigned int cycles_into_pulse;
unsigned int bits_remaining_until_empty;
} _output;
bool _is_running;
bool _is_enabled;
bool _is_in_input_mode;
inline void evaluate_interrupts();
uint16_t _data_register;
uint8_t _interrupt_status, _last_posted_interrupt_status;
Delegate *_delegate;
enum {
Long, Short, Unrecognised, Recognised
} _crossings[4];
};
class Speaker: public ::Outputs::Filter<Speaker> {
public:
void set_divider(uint8_t divider);
void set_is_enabled(bool is_enabled);
inline bool get_is_enabled() { return _is_enabled; }
void get_samples(unsigned int number_of_samples, int16_t *target);
void skip_samples(unsigned int number_of_samples);
private:
unsigned int _counter;
unsigned int _divider;
bool _is_enabled;
};
/*!
@abstract Represents an Acorn Electron.
@discussion An instance of Electron::Machine represents the current state of an
Acorn Electron.
*/
class Machine: public CPU6502::Processor<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; }
private:
inline void update_display();
inline void start_pixel_line();
inline void end_pixel_line();
inline void output_pixels(unsigned int number_of_cycles);
inline void update_audio();
inline void signal_interrupt(Interrupt interrupt);
inline void clear_interrupt(Interrupt interrupt);
inline void evaluate_interrupts();
// Things that directly constitute the memory map.
uint8_t _roms[16][16384];
uint8_t _os[16384], _ram[32768];
// Things affected by registers, explicitly or otherwise.
uint8_t _interrupt_status, _interrupt_control;
uint8_t _palette[16];
uint8_t _key_states[14];
ROMSlot _active_rom;
uint8_t _screen_mode;
uint16_t _screenModeBaseAddress;
uint16_t _startScreenAddress;
// Counters related to simultaneous subsystems
unsigned int _frameCycles, _displayOutputPosition;
unsigned int _audioOutputPosition, _audioOutputPositionError;
uint8_t _phase;
struct {
uint16_t forty1bpp[256];
uint8_t forty2bpp[256];
uint32_t eighty1bpp[256];
uint16_t eighty2bpp[256];
uint8_t eighty4bpp[256];
} _paletteTables;
// Display generation.
uint16_t _startLineAddress, _currentScreenAddress;
int _current_pixel_line, _current_pixel_column, _current_character_row;
uint8_t _last_pixel_byte;
bool _isBlankLine;
// CRT output
uint8_t *_current_output_target, *_initial_output_target;
unsigned int _current_output_divider;
// Tape.
Tape _tape;
bool _use_fast_tape_hack;
bool _fast_load_is_in_data;
// Outputs.
std::unique_ptr<Outputs::CRT::CRT> _crt;
Speaker _speaker;
};
}
#endif /* Electron_hpp */

View File

@ -7,15 +7,27 @@
objects = {
/* Begin PBXBuildFile section */
4B14144E1B5883E500E04248 /* CSAtari2600.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B14144B1B5883E500E04248 /* CSAtari2600.mm */; };
4B14144F1B5883E500E04248 /* CSCathodeRayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B14144D1B5883E500E04248 /* CSCathodeRayView.m */; };
4B1414511B5885DF00E04248 /* Atari2600.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6D7F921B58822000787C9A /* Atari2600.cpp */; };
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CCC421C62D0B3001CAC5F /* CRT.cpp */; };
4B0EBFB81C487F2F00A11F35 /* AudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B0EBFB71C487F2F00A11F35 /* AudioQueue.m */; };
4B14145B1B58879D00E04248 /* CPU6502.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414571B58879D00E04248 /* CPU6502.cpp */; };
4B14145D1B5887A600E04248 /* CPU6502.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414571B58879D00E04248 /* CPU6502.cpp */; };
4B14145E1B5887AA00E04248 /* CPU6502AllRAM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414591B58879D00E04248 /* CPU6502AllRAM.cpp */; };
4B1414601B58885000E04248 /* WolfgangLorenzTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B14145F1B58885000E04248 /* WolfgangLorenzTests.swift */; };
4B1414621B58888700E04248 /* KlausDormannTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414611B58888700E04248 /* KlausDormannTests.swift */; };
4B366DFC1B5C165A0026627B /* CRT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B366DFA1B5C165A0026627B /* CRT.cpp */; };
4B2409551C45AB05004DA684 /* Speaker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2409531C45AB05004DA684 /* Speaker.cpp */; };
4B2E2D951C399D1200138695 /* ElectronDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2E2D931C399D1200138695 /* ElectronDocument.xib */; };
4B2E2D9A1C3A06EC00138695 /* Atari2600.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E2D971C3A06EC00138695 /* Atari2600.cpp */; };
4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E2D9B1C3A070400138695 /* Electron.cpp */; };
4B55CE4B1C3B3B0C0093A61B /* CSAtari2600.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE4A1C3B3B0C0093A61B /* CSAtari2600.mm */; };
4B55CE4E1C3B3BDA0093A61B /* CSMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE4D1C3B3BDA0093A61B /* CSMachine.mm */; };
4B55CE541C3B7ABF0093A61B /* CSElectron.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE531C3B7ABF0093A61B /* CSElectron.mm */; };
4B55CE581C3B7D360093A61B /* Atari2600Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE561C3B7D360093A61B /* Atari2600Document.swift */; };
4B55CE591C3B7D360093A61B /* ElectronDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE571C3B7D360093A61B /* ElectronDocument.swift */; };
4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */; };
4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */; };
4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */; };
4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */; };
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B69FB451C4D950F00B5F0AA /* libz.tbd */; };
4B92EACA1B7C112B00246143 /* TimingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92EAC91B7C112B00246143 /* TimingTests.swift */; };
4BB298EE1B587D8400A49093 /* 6502_functional_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4BB297E01B587D8300A49093 /* 6502_functional_test.bin */; };
4BB298EF1B587D8400A49093 /* AllSuiteA.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4BB297E11B587D8300A49093 /* AllSuiteA.bin */; };
@ -286,12 +298,21 @@
4BB299F81B587D8400A49093 /* txsn in Resources */ = {isa = PBXBuildFile; fileRef = 4BB298EC1B587D8400A49093 /* txsn */; };
4BB299F91B587D8400A49093 /* tyan in Resources */ = {isa = PBXBuildFile; fileRef = 4BB298ED1B587D8400A49093 /* tyan */; };
4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EA11B587A5100552FC2 /* AppDelegate.swift */; };
4BB73EA41B587A5100552FC2 /* Atari2600Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EA31B587A5100552FC2 /* Atari2600Document.swift */; };
4BB73EA71B587A5100552FC2 /* Atari2600Document.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EA51B587A5100552FC2 /* Atari2600Document.xib */; };
4BB73EA91B587A5100552FC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EA81B587A5100552FC2 /* Assets.xcassets */; };
4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */; };
4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EB61B587A5100552FC2 /* AllSuiteATests.swift */; };
4BB73EC21B587A5100552FC2 /* Clock_SignalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */; };
4BBF99141C8FBA6F0075DAFB /* CRTInputBufferBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99081C8FBA6F0075DAFB /* CRTInputBufferBuilder.cpp */; };
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */; };
4BBF99161C8FBA6F0075DAFB /* CRTRunBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF990C1C8FBA6F0075DAFB /* CRTRunBuilder.cpp */; };
4BBF99171C8FBA6F0075DAFB /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99101C8FBA6F0075DAFB /* Shader.cpp */; };
4BBF99181C8FBA6F0075DAFB /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */; };
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; };
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
4BCB70B41C947DDC005B1712 /* plus1.rom in Resources */ = {isa = PBXBuildFile; fileRef = 4BCB70B31C947DDC005B1712 /* plus1.rom */; };
4BE5F85E1C3E1C2500C43F01 /* basic.rom in Resources */ = {isa = PBXBuildFile; fileRef = 4BE5F85C1C3E1C2500C43F01 /* basic.rom */; };
4BE5F85F1C3E1C2500C43F01 /* os.rom in Resources */ = {isa = PBXBuildFile; fileRef = 4BE5F85D1C3E1C2500C43F01 /* os.rom */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -312,10 +333,11 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
4B14144A1B5883E500E04248 /* CSAtari2600.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAtari2600.h; sourceTree = "<group>"; };
4B14144B1B5883E500E04248 /* CSAtari2600.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSAtari2600.mm; sourceTree = "<group>"; };
4B14144C1B5883E500E04248 /* CSCathodeRayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSCathodeRayView.h; sourceTree = "<group>"; };
4B14144D1B5883E500E04248 /* CSCathodeRayView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSCathodeRayView.m; 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>"; };
4B0EBFB61C487F2F00A11F35 /* AudioQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioQueue.h; sourceTree = "<group>"; };
4B0EBFB71C487F2F00A11F35 /* AudioQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioQueue.m; sourceTree = "<group>"; };
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ClockSignal-Bridging-Header.h"; sourceTree = "<group>"; };
4B1414571B58879D00E04248 /* CPU6502.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CPU6502.cpp; sourceTree = "<group>"; };
4B1414581B58879D00E04248 /* CPU6502.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CPU6502.hpp; sourceTree = "<group>"; };
@ -323,13 +345,34 @@
4B14145A1B58879D00E04248 /* CPU6502AllRAM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CPU6502AllRAM.hpp; sourceTree = "<group>"; };
4B14145F1B58885000E04248 /* WolfgangLorenzTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WolfgangLorenzTests.swift; sourceTree = "<group>"; };
4B1414611B58888700E04248 /* KlausDormannTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KlausDormannTests.swift; sourceTree = "<group>"; };
4B2632551B631A510082A461 /* CRTFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CRTFrame.h; path = ../../Outputs/CRTFrame.h; sourceTree = "<group>"; };
4B366DFA1B5C165A0026627B /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CRT.cpp; path = ../../Outputs/CRT.cpp; sourceTree = "<group>"; };
4B366DFB1B5C165A0026627B /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CRT.hpp; path = ../../Outputs/CRT.hpp; sourceTree = "<group>"; };
4B6D7F921B58822000787C9A /* Atari2600.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Atari2600.cpp; sourceTree = "<group>"; };
4B6D7F931B58822000787C9A /* Atari2600.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Atari2600.hpp; sourceTree = "<group>"; };
4B2409531C45AB05004DA684 /* Speaker.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Speaker.cpp; path = ../../Outputs/Speaker.cpp; sourceTree = "<group>"; };
4B2409541C45AB05004DA684 /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Speaker.hpp; path = ../../Outputs/Speaker.hpp; sourceTree = "<group>"; };
4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = "<group>"; };
4B2E2D941C399D1200138695 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ElectronDocument.xib; sourceTree = "<group>"; };
4B2E2D971C3A06EC00138695 /* Atari2600.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Atari2600.cpp; sourceTree = "<group>"; };
4B2E2D981C3A06EC00138695 /* Atari2600.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Atari2600.hpp; sourceTree = "<group>"; };
4B2E2D991C3A06EC00138695 /* Atari2600Inputs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Atari2600Inputs.h; sourceTree = "<group>"; };
4B2E2D9B1C3A070400138695 /* Electron.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Electron.cpp; path = Electron/Electron.cpp; sourceTree = "<group>"; };
4B2E2D9C1C3A070400138695 /* Electron.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Electron.hpp; path = Electron/Electron.hpp; sourceTree = "<group>"; };
4B55CE491C3B3B0C0093A61B /* CSAtari2600.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAtari2600.h; sourceTree = "<group>"; };
4B55CE4A1C3B3B0C0093A61B /* CSAtari2600.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSAtari2600.mm; sourceTree = "<group>"; };
4B55CE4C1C3B3BDA0093A61B /* CSMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSMachine.h; sourceTree = "<group>"; };
4B55CE4D1C3B3BDA0093A61B /* CSMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSMachine.mm; sourceTree = "<group>"; };
4B55CE4F1C3B78A80093A61B /* CSMachine+Subclassing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSMachine+Subclassing.h"; sourceTree = "<group>"; };
4B55CE521C3B7ABF0093A61B /* CSElectron.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSElectron.h; sourceTree = "<group>"; };
4B55CE531C3B7ABF0093A61B /* CSElectron.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSElectron.mm; sourceTree = "<group>"; };
4B55CE561C3B7D360093A61B /* Atari2600Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atari2600Document.swift; sourceTree = "<group>"; };
4B55CE571C3B7D360093A61B /* ElectronDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElectronDocument.swift; sourceTree = "<group>"; };
4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSOpenGLView.h; sourceTree = "<group>"; };
4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSOpenGLView.m; sourceTree = "<group>"; };
4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachineDocument.swift; sourceTree = "<group>"; };
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Tape.cpp; sourceTree = "<group>"; };
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tape.hpp; sourceTree = "<group>"; };
4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TapeUEF.cpp; sourceTree = "<group>"; };
4B69FB431C4D941400B5F0AA /* TapeUEF.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TapeUEF.hpp; sourceTree = "<group>"; };
4B69FB451C4D950F00B5F0AA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
4B92EAC91B7C112B00246143 /* TimingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimingTests.swift; sourceTree = "<group>"; };
4BA3921C1B8402B3007FBF0E /* Atari2600Inputs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Atari2600Inputs.h; sourceTree = "<group>"; };
4BAE587D1C447B7A005B9AF0 /* KeyCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyCodes.h; sourceTree = "<group>"; };
4BB297DF1B587D8200A49093 /* Clock SignalTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Clock SignalTests-Bridging-Header.h"; sourceTree = "<group>"; };
4BB297E01B587D8300A49093 /* 6502_functional_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = 6502_functional_test.bin; sourceTree = "<group>"; };
4BB297E11B587D8300A49093 /* AllSuiteA.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = AllSuiteA.bin; sourceTree = "<group>"; };
@ -602,7 +645,6 @@
4BB298ED1B587D8400A49093 /* tyan */ = {isa = PBXFileReference; lastKnownFileType = file; path = tyan; sourceTree = "<group>"; };
4BB73E9E1B587A5100552FC2 /* Clock Signal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Clock Signal.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4BB73EA11B587A5100552FC2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
4BB73EA31B587A5100552FC2 /* Atari2600Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atari2600Document.swift; sourceTree = "<group>"; };
4BB73EA61B587A5100552FC2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Atari2600Document.xib; sourceTree = "<group>"; };
4BB73EA81B587A5100552FC2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4BB73EAB1B587A5100552FC2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -614,6 +656,25 @@
4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clock_SignalUITests.swift; sourceTree = "<group>"; };
4BB73EC31B587A5100552FC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Clock Signal.entitlements"; sourceTree = "<group>"; };
4BBF99081C8FBA6F0075DAFB /* CRTInputBufferBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRTInputBufferBuilder.cpp; sourceTree = "<group>"; };
4BBF99091C8FBA6F0075DAFB /* CRTInputBufferBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTInputBufferBuilder.hpp; sourceTree = "<group>"; };
4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRTOpenGL.cpp; sourceTree = "<group>"; };
4BBF990B1C8FBA6F0075DAFB /* CRTOpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTOpenGL.hpp; sourceTree = "<group>"; };
4BBF990C1C8FBA6F0075DAFB /* CRTRunBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRTRunBuilder.cpp; sourceTree = "<group>"; };
4BBF990D1C8FBA6F0075DAFB /* CRTRunBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTRunBuilder.hpp; sourceTree = "<group>"; };
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Flywheel.hpp; sourceTree = "<group>"; };
4BBF990F1C8FBA6F0075DAFB /* OpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OpenGL.hpp; sourceTree = "<group>"; };
4BBF99101C8FBA6F0075DAFB /* Shader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shader.cpp; sourceTree = "<group>"; };
4BBF99111C8FBA6F0075DAFB /* Shader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Shader.hpp; sourceTree = "<group>"; };
4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureTarget.cpp; sourceTree = "<group>"; };
4BBF99131C8FBA6F0075DAFB /* TextureTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureTarget.hpp; sourceTree = "<group>"; };
4BBF99191C8FC2750075DAFB /* CRTTypes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTTypes.hpp; sourceTree = "<group>"; };
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FIRFilter.cpp; sourceTree = "<group>"; };
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FIRFilter.hpp; sourceTree = "<group>"; };
4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
4BCB70B31C947DDC005B1712 /* plus1.rom */ = {isa = PBXFileReference; lastKnownFileType = file; path = plus1.rom; sourceTree = "<group>"; };
4BE5F85C1C3E1C2500C43F01 /* basic.rom */ = {isa = PBXFileReference; lastKnownFileType = file; path = basic.rom; sourceTree = "<group>"; };
4BE5F85D1C3E1C2500C43F01 /* os.rom */ = {isa = PBXFileReference; lastKnownFileType = file; path = os.rom; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -621,6 +682,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */,
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -641,6 +704,18 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4B0CCC411C62D0B3001CAC5F /* CRT */ = {
isa = PBXGroup;
children = (
4BBF99071C8FBA6F0075DAFB /* Internals */,
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */,
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */,
4BBF99191C8FC2750075DAFB /* CRTTypes.hpp */,
);
name = CRT;
path = ../../Outputs/CRT;
sourceTree = "<group>";
};
4B1414561B58879D00E04248 /* 6502 */ = {
isa = PBXGroup;
children = (
@ -662,16 +737,113 @@
name = "Test Binaries";
sourceTree = "<group>";
};
4B2409591C45DF85004DA684 /* SignalProcessing */ = {
isa = PBXGroup;
children = (
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */,
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */,
4B24095A1C45DF85004DA684 /* Stepper.hpp */,
);
name = SignalProcessing;
path = ../../SignalProcessing;
sourceTree = "<group>";
};
4B2E2D961C3A06EC00138695 /* Atari2600 */ = {
isa = PBXGroup;
children = (
4B2E2D971C3A06EC00138695 /* Atari2600.cpp */,
4B2E2D981C3A06EC00138695 /* Atari2600.hpp */,
4B2E2D991C3A06EC00138695 /* Atari2600Inputs.h */,
);
path = Atari2600;
sourceTree = "<group>";
};
4B2E2D9E1C3A070900138695 /* Electron */ = {
isa = PBXGroup;
children = (
4B2E2D9B1C3A070400138695 /* Electron.cpp */,
4B2E2D9C1C3A070400138695 /* Electron.hpp */,
);
name = Electron;
sourceTree = "<group>";
};
4B366DFD1B5C165F0026627B /* Outputs */ = {
isa = PBXGroup;
children = (
4B366DFA1B5C165A0026627B /* CRT.cpp */,
4B366DFB1B5C165A0026627B /* CRT.hpp */,
4B2632551B631A510082A461 /* CRTFrame.h */,
4B0CCC411C62D0B3001CAC5F /* CRT */,
4B2409531C45AB05004DA684 /* Speaker.cpp */,
4B2409541C45AB05004DA684 /* Speaker.hpp */,
);
name = Outputs;
sourceTree = "<group>";
};
4B55CE481C3B3B0C0093A61B /* Wrappers */ = {
isa = PBXGroup;
children = (
4B55CE491C3B3B0C0093A61B /* CSAtari2600.h */,
4B55CE4A1C3B3B0C0093A61B /* CSAtari2600.mm */,
4B55CE4C1C3B3BDA0093A61B /* CSMachine.h */,
4B55CE4D1C3B3BDA0093A61B /* CSMachine.mm */,
4B55CE4F1C3B78A80093A61B /* CSMachine+Subclassing.h */,
4B55CE521C3B7ABF0093A61B /* CSElectron.h */,
4B55CE531C3B7ABF0093A61B /* CSElectron.mm */,
4BAE587D1C447B7A005B9AF0 /* KeyCodes.h */,
4B0EBFB61C487F2F00A11F35 /* AudioQueue.h */,
4B0EBFB71C487F2F00A11F35 /* AudioQueue.m */,
);
path = Wrappers;
sourceTree = "<group>";
};
4B55CE551C3B7D360093A61B /* Documents */ = {
isa = PBXGroup;
children = (
4B55CE561C3B7D360093A61B /* Atari2600Document.swift */,
4BB73EA51B587A5100552FC2 /* Atari2600Document.xib */,
4B55CE571C3B7D360093A61B /* ElectronDocument.swift */,
4B2E2D931C399D1200138695 /* ElectronDocument.xib */,
4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */,
);
path = Documents;
sourceTree = "<group>";
};
4B55CE5A1C3B7D6F0093A61B /* Views */ = {
isa = PBXGroup;
children = (
4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */,
4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */,
);
path = Views;
sourceTree = "<group>";
};
4B69FB391C4D908A00B5F0AA /* Storage */ = {
isa = PBXGroup;
children = (
4B69FB3A1C4D908A00B5F0AA /* Tape */,
);
name = Storage;
path = ../../Storage;
sourceTree = "<group>";
};
4B69FB3A1C4D908A00B5F0AA /* Tape */ = {
isa = PBXGroup;
children = (
4B69FB411C4D941400B5F0AA /* Formats */,
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */,
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */,
);
path = Tape;
sourceTree = "<group>";
};
4B69FB411C4D941400B5F0AA /* Formats */ = {
isa = PBXGroup;
children = (
4B69FB451C4D950F00B5F0AA /* libz.tbd */,
4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */,
4B69FB431C4D941400B5F0AA /* TapeUEF.hpp */,
);
path = Formats;
sourceTree = "<group>";
};
4BB297E41B587D8300A49093 /* Wolfgang Lorenz 6502 test suite */ = {
isa = PBXGroup;
children = (
@ -947,13 +1119,16 @@
4BB73E951B587A5100552FC2 = {
isa = PBXGroup;
children = (
4B366DFD1B5C165F0026627B /* Outputs */,
4BB73EDC1B587CA500552FC2 /* Machines */,
4BB73EDD1B587CA500552FC2 /* Processors */,
4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */,
4BB73EA01B587A5100552FC2 /* Clock Signal */,
4BB73EB51B587A5100552FC2 /* Clock SignalTests */,
4BB73EC01B587A5100552FC2 /* Clock SignalUITests */,
4BB73EDC1B587CA500552FC2 /* Machines */,
4B366DFD1B5C165F0026627B /* Outputs */,
4BB73EDD1B587CA500552FC2 /* Processors */,
4BB73E9F1B587A5100552FC2 /* Products */,
4B2409591C45DF85004DA684 /* SignalProcessing */,
4B69FB391C4D908A00B5F0AA /* Storage */,
);
sourceTree = "<group>";
};
@ -970,18 +1145,16 @@
4BB73EA01B587A5100552FC2 /* Clock Signal */ = {
isa = PBXGroup;
children = (
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */,
4B14144A1B5883E500E04248 /* CSAtari2600.h */,
4B14144B1B5883E500E04248 /* CSAtari2600.mm */,
4B14144C1B5883E500E04248 /* CSCathodeRayView.h */,
4B14144D1B5883E500E04248 /* CSCathodeRayView.m */,
4BE5F85A1C3E1C2500C43F01 /* Resources */,
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */,
4BB73EA11B587A5100552FC2 /* AppDelegate.swift */,
4BB73EA31B587A5100552FC2 /* Atari2600Document.swift */,
4BB73EA51B587A5100552FC2 /* Atari2600Document.xib */,
4BB73EA81B587A5100552FC2 /* Assets.xcassets */,
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */,
4BB73EAD1B587A5100552FC2 /* Info.plist */,
4BB73EA11B587A5100552FC2 /* AppDelegate.swift */,
4BB73EA81B587A5100552FC2 /* Assets.xcassets */,
4B55CE551C3B7D360093A61B /* Documents */,
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4B55CE5A1C3B7D6F0093A61B /* Views */,
4B55CE481C3B3B0C0093A61B /* Wrappers */,
);
path = "Clock Signal";
sourceTree = "<group>";
@ -1014,9 +1187,8 @@
4BB73EDC1B587CA500552FC2 /* Machines */ = {
isa = PBXGroup;
children = (
4B6D7F921B58822000787C9A /* Atari2600.cpp */,
4B6D7F931B58822000787C9A /* Atari2600.hpp */,
4BA3921C1B8402B3007FBF0E /* Atari2600Inputs.h */,
4B2E2D961C3A06EC00138695 /* Atari2600 */,
4B2E2D9E1C3A070900138695 /* Electron */,
);
name = Machines;
path = ../../Machines;
@ -1031,6 +1203,44 @@
path = ../../Processors;
sourceTree = "<group>";
};
4BBF99071C8FBA6F0075DAFB /* Internals */ = {
isa = PBXGroup;
children = (
4BBF99081C8FBA6F0075DAFB /* CRTInputBufferBuilder.cpp */,
4BBF99091C8FBA6F0075DAFB /* CRTInputBufferBuilder.hpp */,
4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */,
4BBF990B1C8FBA6F0075DAFB /* CRTOpenGL.hpp */,
4BBF990C1C8FBA6F0075DAFB /* CRTRunBuilder.cpp */,
4BBF990D1C8FBA6F0075DAFB /* CRTRunBuilder.hpp */,
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */,
4BBF990F1C8FBA6F0075DAFB /* OpenGL.hpp */,
4BBF99101C8FBA6F0075DAFB /* Shader.cpp */,
4BBF99111C8FBA6F0075DAFB /* Shader.hpp */,
4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */,
4BBF99131C8FBA6F0075DAFB /* TextureTarget.hpp */,
4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */,
);
path = Internals;
sourceTree = "<group>";
};
4BE5F85A1C3E1C2500C43F01 /* Resources */ = {
isa = PBXGroup;
children = (
4BE5F85B1C3E1C2500C43F01 /* Electron */,
);
path = Resources;
sourceTree = "<group>";
};
4BE5F85B1C3E1C2500C43F01 /* Electron */ = {
isa = PBXGroup;
children = (
4BCB70B31C947DDC005B1712 /* plus1.rom */,
4BE5F85C1C3E1C2500C43F01 /* basic.rom */,
4BE5F85D1C3E1C2500C43F01 /* os.rom */,
);
path = Electron;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1140,9 +1350,13 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4B2E2D951C399D1200138695 /* ElectronDocument.xib in Resources */,
4BB73EA91B587A5100552FC2 /* Assets.xcassets in Resources */,
4BE5F85F1C3E1C2500C43F01 /* os.rom in Resources */,
4BCB70B41C947DDC005B1712 /* plus1.rom in Resources */,
4BB73EA71B587A5100552FC2 /* Atari2600Document.xib in Resources */,
4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */,
4BE5F85E1C3E1C2500C43F01 /* basic.rom in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1434,13 +1648,28 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4B1414511B5885DF00E04248 /* Atari2600.cpp in Sources */,
4B366DFC1B5C165A0026627B /* CRT.cpp in Sources */,
4BB73EA41B587A5100552FC2 /* Atari2600Document.swift in Sources */,
4B14144E1B5883E500E04248 /* CSAtari2600.mm in Sources */,
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */,
4B55CE541C3B7ABF0093A61B /* CSElectron.mm in Sources */,
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */,
4B55CE591C3B7D360093A61B /* ElectronDocument.swift in Sources */,
4B55CE4B1C3B3B0C0093A61B /* CSAtari2600.mm in Sources */,
4BBF99161C8FBA6F0075DAFB /* CRTRunBuilder.cpp in Sources */,
4B55CE581C3B7D360093A61B /* Atari2600Document.swift in Sources */,
4B0EBFB81C487F2F00A11F35 /* AudioQueue.m in Sources */,
4BBF99181C8FBA6F0075DAFB /* TextureTarget.cpp in Sources */,
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */,
4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */,
4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */,
4BBF99141C8FBA6F0075DAFB /* CRTInputBufferBuilder.cpp in Sources */,
4B2409551C45AB05004DA684 /* Speaker.cpp in Sources */,
4B55CE4E1C3B3BDA0093A61B /* CSMachine.mm in Sources */,
4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */,
4BBF99171C8FBA6F0075DAFB /* Shader.cpp in Sources */,
4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */,
4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */,
4B2E2D9A1C3A06EC00138695 /* Atari2600.cpp in Sources */,
4B14145B1B58879D00E04248 /* CPU6502.cpp in Sources */,
4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */,
4B14144F1B5883E500E04248 /* CSCathodeRayView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1482,12 +1711,22 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
4B2E2D931C399D1200138695 /* ElectronDocument.xib */ = {
isa = PBXVariantGroup;
children = (
4B2E2D941C399D1200138695 /* Base */,
);
name = ElectronDocument.xib;
path = ..;
sourceTree = "<group>";
};
4BB73EA51B587A5100552FC2 /* Atari2600Document.xib */ = {
isa = PBXVariantGroup;
children = (
4BB73EA61B587A5100552FC2 /* Base */,
);
name = Atari2600Document.xib;
path = ..;
sourceTree = "<group>";
};
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */ = {

View File

@ -11,8 +11,6 @@ import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
@ -21,6 +19,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Insert code here to tear down your application
}
// decline to open a new file unless the user explicitly requests it
func applicationShouldOpenUntitledFile(sender: NSApplication) -> Bool {
return false
}
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9532" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9532"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="Atari2600Document" customModule="Clock_Signal" customModuleProvider="target">
@ -23,7 +23,7 @@
<rect key="frame" x="0.0" y="0.0" width="400" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<openGLView useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSCathodeRayView">
<openGLView useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView">
<rect key="frame" x="0.0" y="0.0" width="400" height="300"/>
</openGLView>
</subviews>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9532" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9532"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ElectronDocument" customModule="Clock_Signal" customModuleProvider="target">
<connections>
<outlet property="displayTypeButton" destination="rh8-km-57n" id="4Np-OD-NLO"/>
<outlet property="fastLoadingButton" destination="e1J-pw-zGw" id="yck-sy-WRa"/>
<outlet property="openGLView" destination="gIp-Ho-8D9" id="GVg-Gs-Zn9"/>
<outlet property="optionsPanel" destination="ZW7-Bw-4RP" id="JpE-wG-zRR"/>
<outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="xOd-HO-29H" userLabel="Window">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="133" y="235" width="440" height="400"/>
<rect key="screenRect" x="0.0" y="0.0" width="1366" height="768"/>
<value key="minSize" type="size" width="228" height="171"/>
<view key="contentView" canDrawConcurrently="YES" id="gIp-Ho-8D9" customClass="CSOpenGLView">
<rect key="frame" x="0.0" y="0.0" width="440" height="400"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="0bl-1N-x8E"/>
</connections>
</window>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="83" y="102" width="200" height="83"/>
<rect key="screenRect" x="0.0" y="0.0" width="1366" height="768"/>
<value key="minSize" type="size" width="200" height="83"/>
<value key="maxSize" type="size" width="200" height="83"/>
<view key="contentView" id="tpZ-0B-QQu">
<rect key="frame" x="0.0" y="0.0" width="200" height="83"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="e1J-pw-zGw">
<rect key="frame" x="18" y="47" width="164" height="18"/>
<buttonCell key="cell" type="check" title="Load Quickly" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="tD6-UB-ESB">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="setFastLoading:" target="-2" id="CTb-Dn-QiP"/>
</connections>
</button>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rh8-km-57n">
<rect key="frame" x="18" y="17" width="165" height="26"/>
<popUpButtonCell key="cell" type="push" title="Monitor" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tJM-kX-gaK" id="8SX-c5-ud1">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="L06-TO-EF0">
<items>
<menuItem title="Monitor" state="on" id="tJM-kX-gaK"/>
<menuItem title="Television" id="fFm-fS-rWG"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="setDisplayType:" target="-2" id="kgH-SR-fI3"/>
</connections>
</popUpButton>
</subviews>
<constraints>
<constraint firstItem="rh8-km-57n" firstAttribute="top" secondItem="e1J-pw-zGw" secondAttribute="bottom" constant="8" id="GLg-6X-Lj2"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="HSD-3d-Bl7"/>
<constraint firstAttribute="trailing" secondItem="e1J-pw-zGw" secondAttribute="trailing" constant="20" id="Q9M-FH-92N"/>
<constraint firstItem="rh8-km-57n" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="VRo-6R-IKd"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="ul9-lf-Y3u"/>
<constraint firstAttribute="trailing" secondItem="rh8-km-57n" secondAttribute="trailing" constant="20" id="urO-Ac-aqK"/>
</constraints>
</view>
<point key="canvasLocation" x="129" y="46.5"/>
</window>
</objects>
</document>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9532" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9532"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -342,282 +342,14 @@
</items>
</menu>
</menuItem>
<menuItem title="Format" id="jxT-CU-nIS">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
<items>
<menuItem title="Font" id="Gi5-1S-RQB">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
<items>
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
<connections>
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
</connections>
</menuItem>
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
</connections>
</menuItem>
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
</connections>
</menuItem>
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
<connections>
<action selector="underline:" target="-1" id="FYS-2b-JAY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
</connections>
</menuItem>
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
<menuItem title="Kern" id="jBQ-r6-VK2">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
<items>
<menuItem title="Use Default" id="GUa-eO-cwY">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardKerning:" target="-1" id="6dk-9l-Ckg"/>
</connections>
</menuItem>
<menuItem title="Use None" id="cDB-IK-hbR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffKerning:" target="-1" id="U8a-gz-Maa"/>
</connections>
</menuItem>
<menuItem title="Tighten" id="46P-cB-AYj">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="tightenKerning:" target="-1" id="hr7-Nz-8ro"/>
</connections>
</menuItem>
<menuItem title="Loosen" id="ogc-rX-tC1">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="loosenKerning:" target="-1" id="8i4-f9-FKE"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ligatures" id="o6e-r0-MWq">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
<items>
<menuItem title="Use Default" id="agt-UL-0e3">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardLigatures:" target="-1" id="7uR-wd-Dx6"/>
</connections>
</menuItem>
<menuItem title="Use None" id="J7y-lM-qPV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffLigatures:" target="-1" id="iX2-gA-Ilz"/>
</connections>
</menuItem>
<menuItem title="Use All" id="xQD-1f-W4t">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useAllLigatures:" target="-1" id="KcB-kA-TuK"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Baseline" id="OaQ-X3-Vso">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
<items>
<menuItem title="Use Default" id="3Om-Ey-2VK">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unscript:" target="-1" id="0vZ-95-Ywn"/>
</connections>
</menuItem>
<menuItem title="Superscript" id="Rqc-34-cIF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="superscript:" target="-1" id="3qV-fo-wpU"/>
</connections>
</menuItem>
<menuItem title="Subscript" id="I0S-gh-46l">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="subscript:" target="-1" id="Q6W-4W-IGz"/>
</connections>
</menuItem>
<menuItem title="Raise" id="2h7-ER-AoG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="raiseBaseline:" target="-1" id="4sk-31-7Q9"/>
</connections>
</menuItem>
<menuItem title="Lower" id="1tx-W0-xDw">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowerBaseline:" target="-1" id="OF1-bc-KW4"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
<connections>
<action selector="orderFrontColorPanel:" target="-1" id="mSX-Xz-DV3"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="copyFont:" target="-1" id="GJO-xA-L4q"/>
</connections>
</menuItem>
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteFont:" target="-1" id="JfD-CL-leO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Text" id="Fal-I4-PZk">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Text" id="d9c-me-L2H">
<items>
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
<connections>
<action selector="alignLeft:" target="-1" id="zUv-R1-uAa"/>
</connections>
</menuItem>
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
<connections>
<action selector="alignCenter:" target="-1" id="spX-mk-kcS"/>
</connections>
</menuItem>
<menuItem title="Justify" id="J5U-5w-g23">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="alignJustified:" target="-1" id="ljL-7U-jND"/>
</connections>
</menuItem>
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
<connections>
<action selector="alignRight:" target="-1" id="r48-bG-YeY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
<menuItem title="Writing Direction" id="H1b-Si-o9J">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
<items>
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="YGs-j5-SAR">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="qtV-5e-UBP"/>
</connections>
</menuItem>
<menuItem id="Lbh-J2-qVU">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="S0X-9S-QSf"/>
</connections>
</menuItem>
<menuItem id="jFq-tB-4Kx">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="5fk-qB-AqJ"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="Nop-cj-93Q">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionNatural:" target="-1" id="lPI-Se-ZHp"/>
</connections>
</menuItem>
<menuItem id="BgM-ve-c93">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="caW-Bv-w94"/>
</connections>
</menuItem>
<menuItem id="RB4-Sm-HuC">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="EXD-6r-ZUu"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
<menuItem title="Show Ruler" id="vLm-3I-IUL">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleRuler:" target="-1" id="FOx-HJ-KwY"/>
</connections>
</menuItem>
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="copyRuler:" target="-1" id="71i-fW-3W2"/>
</connections>
</menuItem>
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="pasteRuler:" target="-1" id="cSh-wd-qM2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
<menuItem title="Show Options" keyEquivalent="o" id="WCd-6R-baV">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
</connections>
</menuItem>
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runToolbarCustomizationPalette:" target="-1" id="pQI-g3-MTW"/>
<action selector="showOptions:" target="-1" id="ge3-Qg-kb5"/>
</connections>
</menuItem>
</items>

View File

@ -1,114 +0,0 @@
//
// Atari2600.m
// CLK
//
// Created by Thomas Harte on 14/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import "CSAtari2600.h"
#import "Atari2600.hpp"
@interface CSAtari2600 (Callbacks)
- (void)crtDidEndFrame:(CRTFrame *)frame didDetectVSync:(BOOL)didDetectVSync;
@end
struct Atari2600CRTDelegate: public Outputs::CRT::CRTDelegate {
__weak CSAtari2600 *atari;
void crt_did_end_frame(Outputs::CRT *crt, CRTFrame *frame, bool did_detect_vsync) { [atari crtDidEndFrame:frame didDetectVSync:did_detect_vsync]; }
};
typedef NS_ENUM(NSInteger, CSAtari2600RunningState) {
CSAtari2600RunningStateRunning,
CSAtari2600RunningStateStopped
};
@implementation CSAtari2600 {
Atari2600::Machine _atari2600;
Atari2600CRTDelegate _crtDelegate;
dispatch_queue_t _serialDispatchQueue;
int _frameCount;
int _hitCount;
BOOL _didDecideRegion;
NSConditionLock *_runningLock;
}
- (void)crtDidEndFrame:(CRTFrame *)frame didDetectVSync:(BOOL)didDetectVSync {
if(!_didDecideRegion)
{
_frameCount++;
_hitCount += didDetectVSync ? 1 : 0;
if(_frameCount > 30)
{
if(_hitCount < _frameCount >> 1)
{
_atari2600.switch_region();
_didDecideRegion = YES;
}
if(_hitCount > (_frameCount * 7) >> 3)
{
_didDecideRegion = YES;
}
}
}
BOOL hasReturn = [self.view pushFrame:frame];
if(hasReturn)
_atari2600.get_crt()->return_frame();
}
- (void)runForNumberOfCycles:(int)cycles {
if([_runningLock tryLockWhenCondition:CSAtari2600RunningStateStopped]) {
[_runningLock unlockWithCondition:CSAtari2600RunningStateRunning];
dispatch_async(_serialDispatchQueue, ^{
[_runningLock lockWhenCondition:CSAtari2600RunningStateRunning];
_atari2600.run_for_cycles(cycles);
[_runningLock unlockWithCondition:CSAtari2600RunningStateStopped];
});
}
}
- (void)setROM:(NSData *)rom {
dispatch_async(_serialDispatchQueue, ^{
_atari2600.set_rom(rom.length, (const uint8_t *)rom.bytes);
});
}
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput {
dispatch_async(_serialDispatchQueue, ^{
_atari2600.set_digital_input(digitalInput, state ? true : false);
});
}
- (void)setResetLineEnabled:(BOOL)enabled {
dispatch_async(_serialDispatchQueue, ^{
_atari2600.set_reset_line(enabled ? true : false);
});
}
- (void)setView:(CSCathodeRayView *)view {
_view = view;
_view.signalDecoder = [NSString stringWithUTF8String:_atari2600.get_signal_decoder()];
}
- (instancetype)init {
self = [super init];
if (self) {
_crtDelegate.atari = self;
_atari2600.get_crt()->set_delegate(&_crtDelegate);
_serialDispatchQueue = dispatch_queue_create("Atari 2600 queue", DISPATCH_QUEUE_SERIAL);
_runningLock = [[NSConditionLock alloc] initWithCondition:CSAtari2600RunningStateStopped];
}
return self;
}
@end

View File

@ -1,35 +0,0 @@
//
// OpenGLView.h
// Clock Signal
//
// Created by Thomas Harte on 16/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CRTFrame.h"
#import <AppKit/AppKit.h>
@class CSCathodeRayView;
@protocol CSCathodeRayViewDelegate
- (void)openGLView:(nonnull CSCathodeRayView *)view didUpdateToTime:(CVTimeStamp)time;
@end
@protocol CSCathodeRayViewResponderDelegate <NSObject>
- (void)keyDown:(nonnull NSEvent *)event;
- (void)keyUp:(nonnull NSEvent *)event;
- (void)flagsChanged:(nonnull NSEvent *)newModifiers;
@end
@interface CSCathodeRayView : NSOpenGLView
@property (nonatomic, weak) id <CSCathodeRayViewDelegate> delegate;
@property (nonatomic, weak) id <CSCathodeRayViewResponderDelegate> responderDelegate;
- (void)invalidate;
- (BOOL)pushFrame:(nonnull CRTFrame *)crtFrame;
- (void)setSignalDecoder:(nonnull NSString *)signalDecoder;
@end

View File

@ -1,421 +0,0 @@
//
// CSCathodeRayView.m
// CLK
//
// Created by Thomas Harte on 16/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import "CSCathodeRayView.h"
@import CoreVideo;
@import GLKit;
#import <OpenGL/gl3.h>
#import <OpenGL/gl3ext.h>
#import <libkern/OSAtomic.h>
@implementation CSCathodeRayView {
CVDisplayLinkRef displayLink;
GLuint _vertexShader, _fragmentShader;
GLuint _shaderProgram;
GLuint _arrayBuffer, _vertexArray;
GLint _positionAttribute;
GLint _textureCoordinatesAttribute;
GLint _lateralAttribute;
GLint _textureSizeUniform, _windowSizeUniform;
GLint _alphaUniform;
GLuint _textureName, _shadowMaskTextureName;
CRTSize _textureSize;
CRTFrame *_crtFrame;
NSString *_signalDecoder;
int32_t _signalDecoderGeneration;
int32_t _compiledSignalDecoderGeneration;
}
- (GLuint)textureForImageNamed:(NSString *)name
{
NSImage *const image = [NSImage imageNamed:name];
NSBitmapImageRep *bitmapRepresentation = [[NSBitmapImageRep alloc] initWithData: [image TIFFRepresentation]];
GLuint textureName;
glGenTextures(1, &textureName);
glBindTexture(GL_TEXTURE_2D, textureName);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, (GLsizei)image.size.width, (GLsizei)image.size.height, 0, GL_RGB, GL_UNSIGNED_BYTE, bitmapRepresentation.bitmapData);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
return textureName;
}
- (void)prepareOpenGL
{
// Synchronize buffer swaps with vertical refresh rate
GLint swapInt = 1;
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
// Create a display link capable of being used with all active displays
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
// Set the renderer output callback function
CVDisplayLinkSetOutputCallback(displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
// Set the display link for the current renderer
CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, cglContext, cglPixelFormat);
// install the shadow mask texture as the second texture
glActiveTexture(GL_TEXTURE1);
_shadowMaskTextureName = [self textureForImageNamed:@"ShadowMask"];
// otherwise, we'll be working on the first texture
glActiveTexture(GL_TEXTURE0);
// get the shader ready, set the clear colour
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
// Activate the display link
CVDisplayLinkStart(displayLink);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
}
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
{
CSCathodeRayView *view = (__bridge CSCathodeRayView *)displayLinkContext;
[view.delegate openGLView:view didUpdateToTime:*now];
return kCVReturnSuccess;
}
- (void)invalidate
{
CVDisplayLinkStop(displayLink);
}
- (void)dealloc
{
// Release the display link
CVDisplayLinkRelease(displayLink);
// Release OpenGL buffers
[self.openGLContext makeCurrentContext];
glDeleteBuffers(1, &_arrayBuffer);
glDeleteVertexArrays(1, &_vertexArray);
glDeleteTextures(1, &_textureName);
glDeleteTextures(1, &_shadowMaskTextureName);
glDeleteProgram(_shaderProgram);
}
- (void)reshape
{
[super reshape];
[self.openGLContext makeCurrentContext];
CGLLockContext([[self openGLContext] CGLContextObj]);
NSPoint backingSize = {.x = self.bounds.size.width, .y = self.bounds.size.height};
NSPoint viewSize = [self convertPointToBacking:backingSize];
glViewport(0, 0, (GLsizei)viewSize.x, (GLsizei)viewSize.y);
glUniform2f(_windowSizeUniform, (GLfloat)viewSize.x, (GLfloat)viewSize.y);
CGLUnlockContext([[self openGLContext] CGLContextObj]);
}
- (void)awakeFromNib
{
NSOpenGLPixelFormatAttribute attributes[] =
{
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
NSOpenGLPFASampleBuffers, 1,
NSOpenGLPFASamples, 2,
0
};
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
#ifdef DEBUG
// When we're using a CoreProfile context, crash if we call a legacy OpenGL function
// This will make it much more obvious where and when such a function call is made so
// that we can remove such calls.
// Without this we'd simply get GL_INVALID_OPERATION error for calling legacy functions
// but it would be more difficult to see where that function was called.
CGLEnable([context CGLContextObj], kCGLCECrashOnRemovedFunctions);
#endif
self.pixelFormat = pixelFormat;
self.openGLContext = context;
self.wantsBestResolutionOpenGLSurface = YES;
}
- (GLint)formatForDepth:(unsigned int)depth
{
switch(depth)
{
default: return -1;
case 1: return GL_RED;
case 2: return GL_RG;
case 3: return GL_RGB;
case 4: return GL_RGBA;
}
}
- (BOOL)pushFrame:(nonnull CRTFrame *)crtFrame
{
[[self openGLContext] makeCurrentContext];
CGLLockContext([[self openGLContext] CGLContextObj]);
BOOL hadFrame = _crtFrame ? YES : NO;
_crtFrame = crtFrame;
glBufferData(GL_ARRAY_BUFFER, _crtFrame->number_of_runs * kCRTSizeOfVertex * 6, _crtFrame->runs, GL_DYNAMIC_DRAW);
glBindTexture(GL_TEXTURE_2D, _textureName);
if(_textureSize.width != _crtFrame->size.width || _textureSize.height != _crtFrame->size.height)
{
GLint format = [self formatForDepth:_crtFrame->buffers[0].depth];
glTexImage2D(GL_TEXTURE_2D, 0, format, _crtFrame->size.width, _crtFrame->size.height, 0, (GLenum)format, GL_UNSIGNED_BYTE, _crtFrame->buffers[0].data);
_textureSize = _crtFrame->size;
}
else
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _crtFrame->size.width, _crtFrame->dirty_size.height, (GLenum)[self formatForDepth:_crtFrame->buffers[0].depth], GL_UNSIGNED_BYTE, _crtFrame->buffers[0].data);
[self drawView];
CGLUnlockContext([[self openGLContext] CGLContextObj]);
return hadFrame;
}
#pragma mark - Frame output
// the main job of the vertex shader is just to map from an input area of [0,1]x[0,1], with the origin in the
// top left to OpenGL's [-1,1]x[-1,1] with the origin in the lower left, and to convert input data coordinates
// from integral to floating point.
static NSString *const vertexShader =
@"#version 150\n"
"\n"
"in vec2 position;\n"
"in vec2 srcCoordinates;\n"
"in float lateral;\n"
"\n"
"out vec2 srcCoordinatesVarying[4];\n"
"out float lateralVarying;\n"
"out float phase;\n"
"out vec2 shadowMaskCoordinates;\n"
"\n"
"uniform vec2 textureSize;\n"
"\n"
"const float shadowMaskMultiple = 300;\n"
"\n"
"void main (void)\n"
"{\n"
"srcCoordinatesVarying[0] = vec2(srcCoordinates.x / textureSize.x, (srcCoordinates.y + 0.5) / textureSize.y);\n"
"srcCoordinatesVarying[3] = srcCoordinatesVarying[0] + vec2(0.375 / textureSize.x, 0.0);\n"
"srcCoordinatesVarying[2] = srcCoordinatesVarying[0] + vec2(0.125 / textureSize.x, 0.0);\n"
"srcCoordinatesVarying[1] = srcCoordinatesVarying[0] - vec2(0.125 / textureSize.x, 0.0);\n"
"srcCoordinatesVarying[0] = srcCoordinatesVarying[0] - vec2(0.325 / textureSize.x, 0.0);\n"
"\n"
"lateralVarying = lateral + 1.0707963267949;\n"
"phase = srcCoordinates.x * 6.283185308;\n"
"\n"
"shadowMaskCoordinates = position * vec2(shadowMaskMultiple, shadowMaskMultiple * 0.85057471264368);\n"
"\n"
"gl_Position = vec4(position.x * 2.0 - 1.0, 1.0 - position.y * 2.0 + position.x / 131.0, 0.0, 1.0);\n"
"}\n";
// TODO: this should be factored out and be per project
static NSString *const fragmentShader =
@"#version 150\n"
"\n"
"in vec2 srcCoordinatesVarying[4];\n"
"in float lateralVarying;\n"
"in float phase;\n"
"in vec2 shadowMaskCoordinates;\n"
"out vec4 fragColour;\n"
"\n"
"uniform sampler2D texID;\n"
"uniform sampler2D shadowMaskTexID;\n"
"uniform float alpha;\n"
"\n"
"%@"
"\n"
"// for conversion from i and q are in the range [-0.5, 0.5] (so i needs to be multiplied by 1.1914 and q by 1.0452)\n"
"const mat3 yiqToRGB = mat3(1.0, 1.0, 1.0, 1.1389784, -0.3240608, -1.3176884, 0.6490692, -0.6762444, 1.7799756);\n"
"\n"
"void main(void)\n"
"{\n"
"vec4 angles = vec4(phase) + vec4(-2.35619449019234, -0.78539816339745, 0.78539816339745, 2.35619449019234);\n"
"vec4 samples = vec4("
" sample(srcCoordinatesVarying[0], angles.x),"
" sample(srcCoordinatesVarying[1], angles.y),"
" sample(srcCoordinatesVarying[2], angles.z),"
" sample(srcCoordinatesVarying[3], angles.w)"
");\n"
"\n"
"float y = dot(vec4(0.25), samples);\n"
"samples -= vec4(y);\n"
"\n"
"float i = dot(cos(angles), samples);\n"
"float q = dot(sin(angles), samples);\n"
"\n"
"fragColour = 5.0 * texture(shadowMaskTexID, shadowMaskCoordinates) * vec4(yiqToRGB * vec3(y, i, q), 1.0);//sin(lateralVarying));\n"
"}\n";
#if defined(DEBUG)
- (void)logErrorForObject:(GLuint)object
{
GLint logLength;
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc((size_t)logLength);
glGetShaderInfoLog(object, logLength, &logLength, log);
NSLog(@"Compile log:\n%s", log);
free(log);
}
}
#endif
- (GLuint)compileShader:(const char *)source type:(GLenum)type
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
#ifdef DEBUG
[self logErrorForObject:shader];
#endif
return shader;
}
- (void)setSignalDecoder:(nonnull NSString *)signalDecoder
{
_signalDecoder = [signalDecoder copy];
OSAtomicIncrement32(&_signalDecoderGeneration);
}
- (void)prepareShader
{
if(_shaderProgram)
{
glDeleteProgram(_shaderProgram);
glDeleteShader(_vertexShader);
glDeleteShader(_fragmentShader);
}
if(!_signalDecoder)
return;
_shaderProgram = glCreateProgram();
_vertexShader = [self compileShader:[vertexShader UTF8String] type:GL_VERTEX_SHADER];
_fragmentShader = [self compileShader:[[NSString stringWithFormat:fragmentShader, _signalDecoder] UTF8String] type:GL_FRAGMENT_SHADER];
glAttachShader(_shaderProgram, _vertexShader);
glAttachShader(_shaderProgram, _fragmentShader);
glLinkProgram(_shaderProgram);
#ifdef DEBUG
[self logErrorForObject:_shaderProgram];
#endif
glGenVertexArrays(1, &_vertexArray);
glBindVertexArray(_vertexArray);
glGenBuffers(1, &_arrayBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _arrayBuffer);
glUseProgram(_shaderProgram);
_positionAttribute = glGetAttribLocation(_shaderProgram, "position");
_textureCoordinatesAttribute = glGetAttribLocation(_shaderProgram, "srcCoordinates");
_lateralAttribute = glGetAttribLocation(_shaderProgram, "lateral");
_alphaUniform = glGetUniformLocation(_shaderProgram, "alpha");
_textureSizeUniform = glGetUniformLocation(_shaderProgram, "textureSize");
_windowSizeUniform = glGetUniformLocation(_shaderProgram, "windowSize");
GLint texIDUniform = glGetUniformLocation(_shaderProgram, "texID");
GLint shadowMaskTexIDUniform = glGetUniformLocation(_shaderProgram, "shadowMaskTexID");
glUniform1i(texIDUniform, 0);
glUniform1i(shadowMaskTexIDUniform, 1);
glEnableVertexAttribArray((GLuint)_positionAttribute);
glEnableVertexAttribArray((GLuint)_textureCoordinatesAttribute);
glEnableVertexAttribArray((GLuint)_lateralAttribute);
const GLsizei vertexStride = kCRTSizeOfVertex;
glVertexAttribPointer((GLuint)_positionAttribute, 2, GL_UNSIGNED_SHORT, GL_TRUE, vertexStride, (void *)kCRTVertexOffsetOfPosition);
glVertexAttribPointer((GLuint)_textureCoordinatesAttribute, 2, GL_UNSIGNED_SHORT, GL_FALSE, vertexStride, (void *)kCRTVertexOffsetOfTexCoord);
glVertexAttribPointer((GLuint)_lateralAttribute, 1, GL_UNSIGNED_BYTE, GL_FALSE, vertexStride, (void *)kCRTVertexOffsetOfLateral);
glGenTextures(1, &_textureName);
glBindTexture(GL_TEXTURE_2D, _textureName);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}
- (void)drawRect:(NSRect)dirtyRect
{
[self drawView];
}
- (void)drawView
{
[self.openGLContext makeCurrentContext];
CGLLockContext([[self openGLContext] CGLContextObj]);
while(!_shaderProgram || (_signalDecoderGeneration != _compiledSignalDecoderGeneration)) {
_compiledSignalDecoderGeneration = _signalDecoderGeneration;
[self prepareShader];
}
glClear(GL_COLOR_BUFFER_BIT);
if (_crtFrame)
{
glUniform2f(_textureSizeUniform, _crtFrame->size.width, _crtFrame->size.height);
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(_crtFrame->number_of_runs*6));
}
CGLFlushDrawable([[self openGLContext] CGLContextObj]);
CGLUnlockContext([[self openGLContext] CGLContextObj]);
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)keyDown:(NSEvent *)theEvent
{
[self.responderDelegate keyDown:theEvent];
}
- (void)keyUp:(NSEvent *)theEvent
{
[self.responderDelegate keyUp:theEvent];
}
- (void)flagsChanged:(NSEvent *)theEvent
{
[self.responderDelegate flagsChanged:theEvent];
}
@end

View File

@ -2,5 +2,9 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "CSMachine.h"
#import "CSAtari2600.h"
#import "CSCathodeRayView.h"
#import "CSElectron.h"
#import "CSOpenGLView.h"
#import "AudioQueue.h"

View File

@ -8,18 +8,17 @@
import Cocoa
class Atari2600Document: NSDocument, CSCathodeRayViewDelegate, CSCathodeRayViewResponderDelegate {
class Atari2600Document: MachineDocument {
// MARK: NSDocument overrides
override init() {
super.init()
self.intendedCyclesPerSecond = 1194720
}
@IBOutlet weak var openGLView: CSCathodeRayView!
override func windowControllerDidLoadNib(aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
openGLView.delegate = self
openGLView.responderDelegate = self
atari2600!.view = openGLView!
// bind the content aspect ratio to remain 4:3 from now on
aController.window!.contentAspectRatio = NSSize(width: 4.0, height: 3.0)
atari2600.setView(openGLView, aspectRatio: 4.0 / 3.0)
}
override class func autosavesInPlace() -> Bool {
@ -32,7 +31,7 @@ class Atari2600Document: NSDocument, CSCathodeRayViewDelegate, CSCathodeRayViewR
return "Atari2600Document"
}
private var atari2600: CSAtari2600? = nil
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.
@ -40,8 +39,7 @@ class Atari2600Document: NSDocument, CSCathodeRayViewDelegate, CSCathodeRayViewR
}
override func readFromData(data: NSData, ofType typeName: String) throws {
atari2600 = CSAtari2600()
atari2600!.setROM(data)
atari2600.setROM(data)
}
override func close() {
@ -49,31 +47,19 @@ class Atari2600Document: NSDocument, CSCathodeRayViewDelegate, CSCathodeRayViewR
openGLView.invalidate()
}
// MARK: CSOpenGLViewDelegate
// MARK: MachineDocument overrides
private var lastCycleCount: Int64?
func openGLView(view: CSCathodeRayView, didUpdateToTime time: CVTimeStamp) {
override func runForNumberOfCycles(numberOfCycles: Int32) {
atari2600.runForNumberOfCycles(numberOfCycles)
}
// TODO: treat time as a delta from old time, work out how many cycles that is plus error
// this slightly elaborate dance is to avoid overflow
let intendedCyclesPerSecond: Int64 = 1194720
let videoTimeScale64 = Int64(time.videoTimeScale)
let cycleCountLow = ((time.videoTime % videoTimeScale64) * intendedCyclesPerSecond) / videoTimeScale64
let cycleCountHigh = (time.videoTime / videoTimeScale64) * intendedCyclesPerSecond
let cycleCount = cycleCountLow + cycleCountHigh
if let lastCycleCount = lastCycleCount {
let elapsedTime = cycleCount - lastCycleCount
atari2600!.runForNumberOfCycles(Int32(elapsedTime))
}
lastCycleCount = cycleCount
override func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
atari2600.drawViewForPixelSize(view.backingSize, onlyIfDirty: onlyIfDirty)
}
// MARK: CSOpenGLViewResponderDelegate
func inputForKey(event: NSEvent) -> Atari2600DigitalInput? {
private func inputForKey(event: NSEvent) -> Atari2600DigitalInput? {
switch event.keyCode {
case 123: return Atari2600DigitalInputJoy1Left
case 126: return Atari2600DigitalInputJoy1Up
@ -84,26 +70,27 @@ class Atari2600Document: NSDocument, CSCathodeRayViewDelegate, CSCathodeRayViewR
}
}
func keyDown(event: NSEvent) {
override func keyDown(event: NSEvent) {
super.keyDown(event)
if let input = inputForKey(event) {
atari2600!.setState(true, forDigitalInput: input)
atari2600.setState(true, forDigitalInput: input)
}
if event.keyCode == 36 {
atari2600!.setResetLineEnabled(true)
atari2600.setResetLineEnabled(true)
}
}
func keyUp(event: NSEvent) {
override func keyUp(event: NSEvent) {
super.keyUp(event)
if let input = inputForKey(event) {
atari2600!.setState(false, forDigitalInput: input)
atari2600.setState(false, forDigitalInput: input)
}
if event.keyCode == 36 {
atari2600!.setResetLineEnabled(false)
atari2600.setResetLineEnabled(false)
}
}
func flagsChanged(newModifiers: NSEvent) {
}
}

View File

@ -0,0 +1,140 @@
//
// ElectronDocument.swift
// Clock Signal
//
// Created by Thomas Harte on 03/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
import Foundation
import AudioToolbox
class ElectronDocument: MachineDocument {
private lazy var electron = CSElectron()
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
})
establishStoredOptions()
}
override var windowNibName: String? {
return "ElectronDocument"
}
override func readFromURL(url: NSURL, ofType typeName: String) throws {
print(url)
print(typeName)
if let pathExtension = url.pathExtension {
switch pathExtension.lowercaseString {
case "uef":
electron.openUEFAtURL(url)
return
default: break;
}
}
let fileWrapper = try NSFileWrapper(URL: url, options: NSFileWrapperReadingOptions(rawValue: 0))
try self.readFromFileWrapper(fileWrapper, ofType: typeName)
}
override func readFromData(data: NSData, ofType typeName: String) throws {
if let plus1Path = NSBundle.mainBundle().pathForResource("plus1", ofType: "rom") {
electron.setROM(NSData(contentsOfFile: plus1Path)!, slot: 12)
}
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!) {
electron.useTelevisionOutput = (sender.indexOfSelectedItem == 1)
NSUserDefaults.standardUserDefaults().setInteger(sender.indexOfSelectedItem, forKey: self.displayTypeUserDefaultsKey)
}
@IBOutlet var fastLoadingButton: NSButton!
@IBAction func setFastLoading(sender: NSButton!) {
electron.useFastLoadingHack = sender.state == NSOnState
NSUserDefaults.standardUserDefaults().setBool(electron.useFastLoadingHack, forKey: self.fastLoadingUserDefaultsKey)
}
private let displayTypeUserDefaultsKey = "electron.displayType"
private let fastLoadingUserDefaultsKey = "electron.fastLoading"
private func establishStoredOptions() {
let standardUserDefaults = NSUserDefaults.standardUserDefaults()
standardUserDefaults.registerDefaults([
displayTypeUserDefaultsKey: 0,
fastLoadingUserDefaultsKey: true
])
let useFastLoadingHack = standardUserDefaults.boolForKey(self.fastLoadingUserDefaultsKey)
electron.useFastLoadingHack = useFastLoadingHack
self.fastLoadingButton.state = useFastLoadingHack ? NSOnState : NSOffState
let displayType = standardUserDefaults.integerForKey(self.displayTypeUserDefaultsKey)
electron.useTelevisionOutput = (displayType == 1)
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()
}
// MARK: CSOpenGLViewResponderDelegate
override func keyDown(event: NSEvent) {
electron.setKey(event.keyCode, isPressed: true)
}
override func keyUp(event: NSEvent) {
electron.setKey(event.keyCode, isPressed: false)
}
override func flagsChanged(newModifiers: NSEvent) {
electron.setKey(VK_Shift, isPressed: newModifiers.modifierFlags.contains(.ShiftKeyMask))
electron.setKey(VK_Control, isPressed: newModifiers.modifierFlags.contains(.ControlKeyMask))
electron.setKey(VK_Command, isPressed: newModifiers.modifierFlags.contains(.CommandKeyMask))
electron.setKey(VK_Option, isPressed: newModifiers.modifierFlags.contains(.AlternateKeyMask))
}
}

View File

@ -0,0 +1,72 @@
//
// MachineDocument.swift
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
import Cocoa
import AudioToolbox
class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDelegate, NSWindowDelegate {
@IBOutlet weak var openGLView: CSOpenGLView! {
didSet {
openGLView.delegate = self
openGLView.responderDelegate = self
}
}
@IBOutlet weak var optionsPanel: NSPanel!
@IBAction func showOptions(sender: AnyObject!) {
optionsPanel?.setIsVisible(true)
}
lazy var audioQueue = AudioQueue()
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)
}
var intendedCyclesPerSecond: Int64 = 0
private var cycleCountError: Int64 = 0
private var lastTime: CVTimeStamp?
private var skippedFrames = 0
final func openGLView(view: CSOpenGLView, didUpdateToTime time: CVTimeStamp, didSkipPreviousUpdate : Bool, frequency : Double) {
if let lastTime = lastTime {
// perform (time passed in seconds) * (intended cycles per second), converting and
// maintaining an error count to deal with underflow
let videoTimeScale64 = Int64(time.videoTimeScale)
let videoTimeCount = ((time.videoTime - lastTime.videoTime) * intendedCyclesPerSecond) + cycleCountError
cycleCountError = videoTimeCount % videoTimeScale64
var numberOfCycles = videoTimeCount / videoTimeScale64
// if the emulation has fallen behind then silently limit the request;
// some actions e.g. the host computer waking after sleep may give us a
// prohibitive backlog
if didSkipPreviousUpdate {
skippedFrames++
} else {
skippedFrames = 0
}
if skippedFrames > 4 {
numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency))
}
runForNumberOfCycles(Int32(numberOfCycles))
}
lastTime = time
}
func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {}
func runForNumberOfCycles(numberOfCycles: Int32) {}
// MARK: CSOpenGLViewResponderDelegate
func keyDown(event: NSEvent) {}
func keyUp(event: NSEvent) {}
func flagsChanged(newModifiers: NSEvent) {}
}

View File

@ -27,6 +27,53 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).Atari2600Document</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>uef</string>
</array>
<key>CFBundleTypeName</key>
<string>Electron/BBC Tape Image</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSItemContentTypes</key>
<array/>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).ElectronDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>rom</string>
</array>
<key>CFBundleTypeName</key>
<string>Electron/BBC ROM Image</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array/>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).ElectronDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>uef</string>
<string>uef.gz</string>
</array>
<key>CFBundleTypeName</key>
<string>Electron/BBC UEF Image</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).ElectronDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@ -0,0 +1,78 @@
//
// CSOpenGLView.h
// Clock Signal
//
// Created by Thomas Harte on 16/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
@class CSOpenGLView;
@protocol CSOpenGLViewDelegate
/*!
Tells the delegate that time has advanced.
@param view The view sending the message.
@param time The time to which time has advanced.
@param didSkipPreviousUpdate @c YES if the previous update that would have occurred was skipped because a didUpdateToTime: call prior to that was still ongoing; @c NO otherwise.
*/
- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateToTime:(CVTimeStamp)time didSkipPreviousUpdate:(BOOL)didSkipPreviousUpdate frequency:(double)frequency;
/*!
Requests that the delegate produce an image of its current output state. May be called on
any queue or thread.
@param view The view makin the request.
@param onlyIfDirty If @c YES then the delegate may decline to redraw if its output would be
identical to the previous frame. If @c NO then the delegate must draw.
*/
- (void)openGLView:(nonnull CSOpenGLView *)view drawViewOnlyIfDirty:(BOOL)onlyIfDirty;
@end
@protocol CSOpenGLViewResponderDelegate <NSObject>
/*!
Supplies a keyDown event to the delegate. Will always be called on the same queue as the other
@c CSOpenGLViewResponderDelegate methods and as -[CSOpenGLViewDelegate openGLView:didUpdateToTime:].
@param event The @c NSEvent describing the keyDown.
*/
- (void)keyDown:(nonnull NSEvent *)event;
/*!
Supplies a keyUp event to the delegate. Will always be called on the same queue as the other
@c CSOpenGLViewResponderDelegate methods and as -[CSOpenGLViewDelegate openGLView:didUpdateToTime:].
@param event The @c NSEvent describing the keyUp.
*/
- (void)keyUp:(nonnull NSEvent *)event;
/*!
Supplies a flagsChanged event to the delegate. Will always be called on the same queue as the other
@c CSOpenGLViewResponderDelegate methods and as -[CSOpenGLViewDelegate openGLView:didUpdateToTime:].
@param event The @c NSEvent describing the flagsChanged.
*/
- (void)flagsChanged:(nonnull NSEvent *)newModifiers;
@end
/*!
Provides an OpenGL canvas with a refresh-linked update timer and manages a serial dispatch queue
such that a delegate may produce video and respond to keyboard events.
*/
@interface CSOpenGLView : NSOpenGLView
@property (nonatomic, weak) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak) id <CSOpenGLViewResponderDelegate> responderDelegate;
/*!
Ends the timer tracking time; should be called prior to giving up the last owning reference
to ensure that any retain cycles implied by the timer are resolved.
*/
- (void)invalidate;
/// The size in pixels of the OpenGL canvas, factoring in screen pixel density and view size in points.
@property (nonatomic, readonly) CGSize backingSize;
- (void)performWithGLContext:(nonnull dispatch_block_t)action;
@end

View File

@ -0,0 +1,178 @@
//
// CSOpenGLView
// CLK
//
// Created by Thomas Harte on 16/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import "CSOpenGLView.h"
@import CoreVideo;
@import GLKit;
@implementation CSOpenGLView {
CVDisplayLinkRef _displayLink;
uint32_t _updateIsOngoing;
BOOL _hasSkipped;
}
- (void)prepareOpenGL
{
// Synchronize buffer swaps with vertical refresh rate
GLint swapInt = 1;
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
// Create a display link capable of being used with all active displays
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
// Set the renderer output callback function
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
// Set the display link for the current renderer
CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
// set the clear colour
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
// Activate the display link
CVDisplayLinkStart(_displayLink);
}
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
{
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
return kCVReturnSuccess;
}
- (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.
if(!OSAtomicTestAndSet(processingMask, &_updateIsOngoing))
{
CVTimeStamp time = *now;
BOOL didSkip = _hasSkipped;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self.delegate openGLView:self didUpdateToTime:time didSkipPreviousUpdate:didSkip frequency:frequency];
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))
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self drawViewOnlyIfDirty:YES];
OSAtomicTestAndClear(drawingMask, &_updateIsOngoing);
});
}
}
- (void)invalidate
{
CVDisplayLinkStop(_displayLink);
}
- (void)dealloc
{
// Release the display link
CVDisplayLinkRelease(_displayLink);
}
- (CGSize)backingSize
{
return [self convertSizeToBacking:self.bounds.size];
}
- (void)reshape
{
[super reshape];
[self performWithGLContext:^{
CGSize viewSize = [self backingSize];
glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height);
}];
}
- (void)awakeFromNib
{
NSOpenGLPixelFormatAttribute attributes[] =
{
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
NSOpenGLPFAMultisample,
NSOpenGLPFASampleBuffers, 1,
NSOpenGLPFASamples, 2,
0
};
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
#ifdef DEBUG
// When we're using a CoreProfile context, crash if we call a legacy OpenGL function
// This will make it much more obvious where and when such a function call is made so
// that we can remove such calls.
// Without this we'd simply get GL_INVALID_OPERATION error for calling legacy functions
// but it would be more difficult to see where that function was called.
CGLEnable([context CGLContextObj], kCGLCECrashOnRemovedFunctions);
#endif
self.pixelFormat = pixelFormat;
self.openGLContext = context;
self.wantsBestResolutionOpenGLSurface = YES;
}
- (void)drawRect:(NSRect)dirtyRect
{
[self drawViewOnlyIfDirty:NO];
}
- (void)drawViewOnlyIfDirty:(BOOL)onlyIfDirty
{
[self performWithGLContext:^{
[self.delegate openGLView:self drawViewOnlyIfDirty:onlyIfDirty];
CGLFlushDrawable([[self openGLContext] CGLContextObj]);
}];
}
- (void)performWithGLContext:(dispatch_block_t)action
{
CGLLockContext([[self openGLContext] CGLContextObj]);
[self.openGLContext makeCurrentContext];
action();
CGLUnlockContext([[self openGLContext] CGLContextObj]);
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)keyDown:(NSEvent *)theEvent
{
[self.responderDelegate keyDown:theEvent];
}
- (void)keyUp:(NSEvent *)theEvent
{
[self.responderDelegate keyUp:theEvent];
}
- (void)flagsChanged:(NSEvent *)theEvent
{
[self.responderDelegate flagsChanged:theEvent];
}
@end

View File

@ -0,0 +1,15 @@
//
// AudioQueue.h
// Clock Signal
//
// Created by Thomas Harte on 14/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface AudioQueue : NSObject
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples;
@end

View File

@ -0,0 +1,193 @@
//
// AudioQueue.m
// Clock Signal
//
// Created by Thomas Harte on 14/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import "AudioQueue.h"
@import AudioToolbox;
#define AudioQueueNumAudioBuffers 4
#define AudioQueueStreamLength 1024
#define AudioQueueBufferLength 256
enum {
AudioQueueCanProceed,
AudioQueueWait,
AudioQueueIsInvalidated
};
@implementation AudioQueue
{
AudioQueueRef _audioQueue;
AudioQueueBufferRef _audioBuffers[AudioQueueNumAudioBuffers];
unsigned int _audioStreamReadPosition, _audioStreamWritePosition;
int16_t _audioStream[AudioQueueStreamLength];
NSConditionLock *_writeLock;
BOOL _isInvalidated;
int _dequeuedCount;
}
#pragma mark -
#pragma mark AudioQueue callbacks and setup; for pushing audio out
- (void)audioQueue:(AudioQueueRef)theAudioQueue didCallbackWithBuffer:(AudioQueueBufferRef)buffer
{
[_writeLock lock];
const unsigned int writeLead = _audioStreamWritePosition - _audioStreamReadPosition;
const size_t audioDataSampleSize = buffer->mAudioDataByteSize / sizeof(int16_t);
// TODO: if write lead is too great, skip some audio
if(writeLead >= audioDataSampleSize)
{
size_t samplesBeforeOverflow = AudioQueueStreamLength - (_audioStreamReadPosition % AudioQueueStreamLength);
if(audioDataSampleSize <= samplesBeforeOverflow)
{
memcpy(buffer->mAudioData, &_audioStream[_audioStreamReadPosition % AudioQueueStreamLength], buffer->mAudioDataByteSize);
}
else
{
const size_t bytesRemaining = samplesBeforeOverflow * sizeof(int16_t);
memcpy(buffer->mAudioData, &_audioStream[_audioStreamReadPosition % AudioQueueStreamLength], bytesRemaining);
memcpy(buffer->mAudioData, &_audioStream[0], buffer->mAudioDataByteSize - bytesRemaining);
}
_audioStreamReadPosition += audioDataSampleSize;
}
else
{
memset(buffer->mAudioData, 0, buffer->mAudioDataByteSize);
}
if(!_isInvalidated)
{
[_writeLock unlockWithCondition:AudioQueueCanProceed];
AudioQueueEnqueueBuffer(theAudioQueue, buffer, 0, NULL);
}
else
{
_dequeuedCount++;
if(_dequeuedCount == AudioQueueNumAudioBuffers)
[_writeLock unlockWithCondition:AudioQueueIsInvalidated];
else
[_writeLock unlockWithCondition:AudioQueueCanProceed];
}
}
static void audioOutputCallback(
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer)
{
[(__bridge AudioQueue *)inUserData audioQueue:inAQ didCallbackWithBuffer:inBuffer];
}
- (instancetype)init
{
self = [super init];
if(self)
{
_writeLock = [[NSConditionLock alloc] initWithCondition:AudioQueueCanProceed];
/*
Describe a mono, 16bit, 44.1Khz audio format
*/
AudioStreamBasicDescription outputDescription;
outputDescription.mSampleRate = 44100;
outputDescription.mFormatID = kAudioFormatLinearPCM;
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
outputDescription.mBytesPerPacket = 2;
outputDescription.mFramesPerPacket = 1;
outputDescription.mBytesPerFrame = 2;
outputDescription.mChannelsPerFrame = 1;
outputDescription.mBitsPerChannel = 16;
outputDescription.mReserved = 0;
// create an audio output queue along those lines
if(!AudioQueueNewOutput(
&outputDescription,
audioOutputCallback,
(__bridge void *)(self),
NULL,
kCFRunLoopCommonModes,
0,
&_audioQueue))
{
UInt32 bufferBytes = AudioQueueBufferLength * sizeof(int16_t);
int c = AudioQueueNumAudioBuffers;
while(c--)
{
AudioQueueAllocateBuffer(_audioQueue, bufferBytes, &_audioBuffers[c]);
memset(_audioBuffers[c]->mAudioData, 0, bufferBytes);
_audioBuffers[c]->mAudioDataByteSize = bufferBytes;
AudioQueueEnqueueBuffer(_audioQueue, _audioBuffers[c], 0, NULL);
}
AudioQueueStart(_audioQueue, NULL);
}
}
return self;
}
- (void)dealloc
{
[_writeLock lock];
_isInvalidated = YES;
[_writeLock unlock];
[_writeLock lockWhenCondition:AudioQueueIsInvalidated];
[_writeLock unlock];
int c = AudioQueueNumAudioBuffers;
while(c--)
AudioQueueFreeBuffer(_audioQueue, _audioBuffers[c]);
if(_audioQueue) AudioQueueDispose(_audioQueue, NO);
}
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples
{
while(1)
{
[_writeLock lockWhenCondition:AudioQueueCanProceed];
if((_audioStreamReadPosition + AudioQueueStreamLength) - _audioStreamWritePosition >= lengthInSamples)
{
size_t samplesBeforeOverflow = AudioQueueStreamLength - (_audioStreamWritePosition % AudioQueueStreamLength);
if(samplesBeforeOverflow < lengthInSamples)
{
memcpy(&_audioStream[_audioStreamWritePosition % AudioQueueStreamLength], buffer, samplesBeforeOverflow * sizeof(int16_t));
memcpy(&_audioStream[0], &buffer[samplesBeforeOverflow], (lengthInSamples - samplesBeforeOverflow) * sizeof(int16_t));
}
else
{
memcpy(&_audioStream[_audioStreamWritePosition % AudioQueueStreamLength], buffer, lengthInSamples * sizeof(int16_t));
}
_audioStreamWritePosition += lengthInSamples;
[_writeLock unlockWithCondition:[self writeLockCondition]];
break;
}
else
{
[_writeLock unlockWithCondition:AudioQueueWait];
}
}
}
- (NSInteger)writeLockCondition
{
return ((_audioStreamWritePosition - _audioStreamReadPosition) < (AudioQueueStreamLength - AudioQueueBufferLength)) ? AudioQueueCanProceed : AudioQueueWait;
}
@end

View File

@ -6,18 +6,15 @@
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CSCathodeRayView.h"
#include "CSMachine.h"
#include "Atari2600Inputs.h"
@interface CSAtari2600 : NSObject
@interface CSAtari2600 : CSMachine
@property (nonatomic, weak) CSCathodeRayView *view;
- (void)runForNumberOfCycles:(int)cycles;
- (void)setROM:(nonnull NSData *)rom;
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput;
- (void)setResetLineEnabled:(BOOL)enabled;
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
@end

View File

@ -0,0 +1,94 @@
//
// Atari2600.m
// CLK
//
// Created by Thomas Harte on 14/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#import "CSAtari2600.h"
#import "Atari2600.hpp"
#import "CSMachine+Subclassing.h"
@interface CSAtari2600 ()
- (void)crt:(Outputs::CRT::CRT *)crt didEndBatchOfFrames:(unsigned int)numberOfFrames withUnexpectedVerticalSyncs:(unsigned int)numberOfUnexpectedSyncs;
@end
struct CRTDelegate: public Outputs::CRT::Delegate {
__weak CSAtari2600 *atari2600;
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) {
[atari2600 crt:crt didEndBatchOfFrames:number_of_frames withUnexpectedVerticalSyncs:number_of_unexpected_vertical_syncs];
}
};
@implementation CSAtari2600 {
Atari2600::Machine _atari2600;
CRTDelegate _crtDelegate;
int _frameCount;
int _hitCount;
BOOL _didDecideRegion;
int _batchesReceived;
}
- (void)crt:(Outputs::CRT::CRT *)crt didEndBatchOfFrames:(unsigned int)numberOfFrames withUnexpectedVerticalSyncs:(unsigned int)numberOfUnexpectedSyncs {
if(!_didDecideRegion)
{
_batchesReceived++;
if(_batchesReceived == 2)
{
_didDecideRegion = YES;
if(numberOfUnexpectedSyncs >= numberOfFrames >> 1)
{
[self.view performWithGLContext:^{
_atari2600.switch_region();
}];
}
}
}
}
- (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);
}
}
- (void)setState:(BOOL)state forDigitalInput:(Atari2600DigitalInput)digitalInput {
@synchronized(self) {
_atari2600.set_digital_input(digitalInput, state ? true : false);
}
}
- (void)setResetLineEnabled:(BOOL)enabled {
@synchronized(self) {
_atari2600.set_reset_line(enabled ? true : false);
}
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) {
_atari2600.setup_output(aspectRatio);
_atari2600.get_crt()->set_delegate(&_crtDelegate);
_crtDelegate.atari2600 = self;
}
}
- (void)closeOutput {
@synchronized(self) {
_atari2600.close_output();
}
}
@end

View File

@ -0,0 +1,27 @@
//
// CSElectron.h
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "CSMachine.h"
#import "KeyCodes.h"
@interface CSElectron : CSMachine
- (void)setOSROM:(nonnull NSData *)rom;
- (void)setBASICROM:(nonnull NSData *)rom;
- (void)setROM:(nonnull NSData *)rom slot:(int)slot;
- (BOOL)openUEFAtURL:(nonnull NSURL *)URL;
- (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;
@end

View File

@ -0,0 +1,180 @@
//
// CSElectron.m
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import "CSElectron.h"
#import "Electron.hpp"
#import "CSMachine+Subclassing.h"
#import "TapeUEF.hpp"
@implementation CSElectron {
Electron::Machine _electron;
}
- (void)runForNumberOfCycles:(int)numberOfCycles {
@synchronized(self) {
_electron.run_for_cycles(numberOfCycles);
_electron.update_output();
}
}
- (void)setOSROM:(nonnull NSData *)rom {
@synchronized(self) {
_electron.set_rom(Electron::ROMSlotOS, rom.length, (const uint8_t *)rom.bytes);
}
}
- (void)setBASICROM:(nonnull NSData *)rom {
@synchronized(self) {
_electron.set_rom(Electron::ROMSlotBASIC, rom.length, (const uint8_t *)rom.bytes);
}
}
- (void)setROM:(nonnull NSData *)rom slot:(int)slot {
@synchronized(self) {
_electron.set_rom((Electron::ROMSlot)slot, rom.length, (const uint8_t *)rom.bytes);
}
}
- (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 {
std::shared_ptr<Storage::UEF> tape(new Storage::UEF([URL fileSystemRepresentation]));
_electron.set_tape(tape);
return YES;
} catch(int exception) {
return NO;
}
}
}
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate {
@synchronized(self) {
_electron.get_speaker()->set_output_rate(sampleRate, 256);
_electron.get_speaker()->set_delegate(delegate);
return YES;
}
}
- (void)clearAllKeys {
@synchronized(self) {
_electron.clear_all_keys();
}
}
- (void)setKey:(uint16_t)key isPressed:(BOOL)isPressed {
@synchronized(self) {
switch(key)
{
case VK_ANSI_0: _electron.set_key_state(Electron::Key::Key0, isPressed); break;
case VK_ANSI_1: _electron.set_key_state(Electron::Key::Key1, isPressed); break;
case VK_ANSI_2: _electron.set_key_state(Electron::Key::Key2, isPressed); break;
case VK_ANSI_3: _electron.set_key_state(Electron::Key::Key3, isPressed); break;
case VK_ANSI_4: _electron.set_key_state(Electron::Key::Key4, isPressed); break;
case VK_ANSI_5: _electron.set_key_state(Electron::Key::Key5, isPressed); break;
case VK_ANSI_6: _electron.set_key_state(Electron::Key::Key6, isPressed); break;
case VK_ANSI_7: _electron.set_key_state(Electron::Key::Key7, isPressed); break;
case VK_ANSI_8: _electron.set_key_state(Electron::Key::Key8, isPressed); break;
case VK_ANSI_9: _electron.set_key_state(Electron::Key::Key9, isPressed); break;
case VK_ANSI_Q: _electron.set_key_state(Electron::Key::KeyQ, isPressed); break;
case VK_ANSI_W: _electron.set_key_state(Electron::Key::KeyW, isPressed); break;
case VK_ANSI_E: _electron.set_key_state(Electron::Key::KeyE, isPressed); break;
case VK_ANSI_R: _electron.set_key_state(Electron::Key::KeyR, isPressed); break;
case VK_ANSI_T: _electron.set_key_state(Electron::Key::KeyT, isPressed); break;
case VK_ANSI_Y: _electron.set_key_state(Electron::Key::KeyY, isPressed); break;
case VK_ANSI_U: _electron.set_key_state(Electron::Key::KeyU, isPressed); break;
case VK_ANSI_I: _electron.set_key_state(Electron::Key::KeyI, isPressed); break;
case VK_ANSI_O: _electron.set_key_state(Electron::Key::KeyO, isPressed); break;
case VK_ANSI_P: _electron.set_key_state(Electron::Key::KeyP, isPressed); break;
case VK_ANSI_A: _electron.set_key_state(Electron::Key::KeyA, isPressed); break;
case VK_ANSI_S: _electron.set_key_state(Electron::Key::KeyS, isPressed); break;
case VK_ANSI_D: _electron.set_key_state(Electron::Key::KeyD, isPressed); break;
case VK_ANSI_F: _electron.set_key_state(Electron::Key::KeyF, isPressed); break;
case VK_ANSI_G: _electron.set_key_state(Electron::Key::KeyG, isPressed); break;
case VK_ANSI_H: _electron.set_key_state(Electron::Key::KeyH, isPressed); break;
case VK_ANSI_J: _electron.set_key_state(Electron::Key::KeyJ, isPressed); break;
case VK_ANSI_K: _electron.set_key_state(Electron::Key::KeyK, isPressed); break;
case VK_ANSI_L: _electron.set_key_state(Electron::Key::KeyL, isPressed); break;
case VK_ANSI_Z: _electron.set_key_state(Electron::Key::KeyZ, isPressed); break;
case VK_ANSI_X: _electron.set_key_state(Electron::Key::KeyX, isPressed); break;
case VK_ANSI_C: _electron.set_key_state(Electron::Key::KeyC, isPressed); break;
case VK_ANSI_V: _electron.set_key_state(Electron::Key::KeyV, isPressed); break;
case VK_ANSI_B: _electron.set_key_state(Electron::Key::KeyB, isPressed); break;
case VK_ANSI_N: _electron.set_key_state(Electron::Key::KeyN, isPressed); break;
case VK_ANSI_M: _electron.set_key_state(Electron::Key::KeyM, isPressed); break;
case VK_Space: _electron.set_key_state(Electron::Key::KeySpace, isPressed); break;
case VK_ANSI_Grave:
case VK_ANSI_Backslash:
_electron.set_key_state(Electron::Key::KeyCopy, isPressed); break;
case VK_Return: _electron.set_key_state(Electron::Key::KeyReturn, isPressed); break;
case VK_ANSI_Minus: _electron.set_key_state(Electron::Key::KeyMinus, isPressed); break;
case VK_RightArrow: _electron.set_key_state(Electron::Key::KeyRight, isPressed); break;
case VK_LeftArrow: _electron.set_key_state(Electron::Key::KeyLeft, isPressed); break;
case VK_DownArrow: _electron.set_key_state(Electron::Key::KeyDown, isPressed); break;
case VK_UpArrow: _electron.set_key_state(Electron::Key::KeyUp, isPressed); break;
case VK_Delete: _electron.set_key_state(Electron::Key::KeyDelete, isPressed); break;
case VK_Escape: _electron.set_key_state(Electron::Key::KeyEscape, isPressed); break;
case VK_ANSI_Comma: _electron.set_key_state(Electron::Key::KeyComma, isPressed); break;
case VK_ANSI_Period: _electron.set_key_state(Electron::Key::KeyFullStop, isPressed); break;
case VK_ANSI_Semicolon:
_electron.set_key_state(Electron::Key::KeySemiColon, isPressed); break;
case VK_ANSI_Quote: _electron.set_key_state(Electron::Key::KeyColon, isPressed); break;
case VK_ANSI_Slash: _electron.set_key_state(Electron::Key::KeySlash, isPressed); break;
case VK_Shift: _electron.set_key_state(Electron::Key::KeyShift, isPressed); break;
case VK_Control: _electron.set_key_state(Electron::Key::KeyControl, isPressed); break;
case VK_Command:
case VK_Option: _electron.set_key_state(Electron::Key::KeyFunc, isPressed); break;
case VK_F12: _electron.set_key_state(Electron::Key::KeyBreak, isPressed); break;
default:
// printf("%02x\n", key);
break;
}
}
}
- (void)setUseFastLoadingHack:(BOOL)useFastLoadingHack {
@synchronized(self) {
_useFastLoadingHack = useFastLoadingHack;
_electron.set_use_fast_tape_hack(useFastLoadingHack ? true : false);
}
}
- (void)setUseTelevisionOutput:(BOOL)useTelevisionOutput {
@synchronized(self) {
_useTelevisionOutput = useTelevisionOutput;
_electron.get_crt()->set_output_device(useTelevisionOutput ? Outputs::CRT::Television : Outputs::CRT::Monitor);
}
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) {
_electron.setup_output(aspectRatio);
}
}
- (void)closeOutput {
@synchronized(self) {
_electron.close_output();
}
}
@end

View File

@ -0,0 +1,22 @@
//
// CSMachine+Subclassing.h
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import "CSMachine.h"
#include "CRT.hpp"
#include "Speaker.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;
- (void)setupOutputWithAspectRatio:(float)aspectRatio;
- (void)closeOutput;
@end

View File

@ -0,0 +1,21 @@
//
// CSMachine.h
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CSOpenGLView.h"
#import "AudioQueue.h"
@interface CSMachine : NSObject
- (void)runForNumberOfCycles:(int)numberOfCycles;
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio;
@property (nonatomic, weak) AudioQueue *audioQueue;
@property (nonatomic, readonly) CSOpenGLView *view;
@end

View File

@ -0,0 +1,71 @@
//
// CSMachine.m
// Clock Signal
//
// Created by Thomas Harte on 04/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import "CSMachine.h"
#import "CSMachine+Subclassing.h"
struct SpeakerDelegate: public Outputs::Speaker::Delegate {
__weak CSMachine *machine;
void speaker_did_complete_samples(Outputs::Speaker *speaker, const int16_t *buffer, int buffer_size) {
[machine speaker:speaker didCompleteSamples:buffer length:buffer_size];
}
};
@implementation CSMachine {
SpeakerDelegate _speakerDelegate;
dispatch_queue_t _serialDispatchQueue;
}
- (void)speaker:(Outputs::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length {
[self.audioQueue enqueueAudioBuffer:samples numberOfSamples:(unsigned int)length];
}
- (instancetype)init {
self = [super init];
if(self) {
_serialDispatchQueue = dispatch_queue_create("Machine queue", DISPATCH_QUEUE_SERIAL);
_speakerDelegate.machine = self;
[self setSpeakerDelegate:&_speakerDelegate sampleRate:44100];
}
return self;
}
- (void)dealloc {
[_view performWithGLContext:^{
[self closeOutput];
}];
}
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Delegate *)delegate sampleRate:(int)sampleRate {
return NO;
}
- (void)runForNumberOfCycles:(int)numberOfCycles {}
- (void)performSync:(dispatch_block_t)action {
dispatch_sync(_serialDispatchQueue, action);
}
- (void)performAsync:(dispatch_block_t)action {
dispatch_async(_serialDispatchQueue, action);
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {}
- (void)closeOutput {}
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
_view = view;
[view performWithGLContext:^{
[self setupOutputWithAspectRatio:aspectRatio];
}];
}
@end

View File

@ -0,0 +1,151 @@
//
// KeyCodes.h
// Clock Signal
//
// Emancipated from Carbon's HIToolbox by Thomas Harte on 11/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef KeyCodes_h
#define KeyCodes_h
/*
Carbon somehow still manages to get into the unit test target; I can't figure out how. So I've
renamed these contants from their origina kVK prefixes to just VK.
*/
/*
* Summary:
* Virtual keycodes
*
* Discussion:
* These constants are the virtual keycodes defined originally in
* Inside Mac Volume V, pg. V-191. They identify physical keys on a
* keyboard. Those constants with "ANSI" in the name are labeled
* according to the key position on an ANSI-standard US keyboard.
* For example, kVK_ANSI_A indicates the virtual keycode for the key
* with the letter 'A' in the US keyboard layout. Other keyboard
* layouts may have the 'A' key label on a different physical key;
* in this case, pressing 'A' will generate a different virtual
* keycode.
*/
enum: uint16_t {
VK_ANSI_A = 0x00,
VK_ANSI_S = 0x01,
VK_ANSI_D = 0x02,
VK_ANSI_F = 0x03,
VK_ANSI_H = 0x04,
VK_ANSI_G = 0x05,
VK_ANSI_Z = 0x06,
VK_ANSI_X = 0x07,
VK_ANSI_C = 0x08,
VK_ANSI_V = 0x09,
VK_ANSI_B = 0x0B,
VK_ANSI_Q = 0x0C,
VK_ANSI_W = 0x0D,
VK_ANSI_E = 0x0E,
VK_ANSI_R = 0x0F,
VK_ANSI_Y = 0x10,
VK_ANSI_T = 0x11,
VK_ANSI_1 = 0x12,
VK_ANSI_2 = 0x13,
VK_ANSI_3 = 0x14,
VK_ANSI_4 = 0x15,
VK_ANSI_6 = 0x16,
VK_ANSI_5 = 0x17,
VK_ANSI_Equal = 0x18,
VK_ANSI_9 = 0x19,
VK_ANSI_7 = 0x1A,
VK_ANSI_Minus = 0x1B,
VK_ANSI_8 = 0x1C,
VK_ANSI_0 = 0x1D,
VK_ANSI_RightBracket = 0x1E,
VK_ANSI_O = 0x1F,
VK_ANSI_U = 0x20,
VK_ANSI_LeftBracket = 0x21,
VK_ANSI_I = 0x22,
VK_ANSI_P = 0x23,
VK_ANSI_L = 0x25,
VK_ANSI_J = 0x26,
VK_ANSI_Quote = 0x27,
VK_ANSI_K = 0x28,
VK_ANSI_Semicolon = 0x29,
VK_ANSI_Backslash = 0x2A,
VK_ANSI_Comma = 0x2B,
VK_ANSI_Slash = 0x2C,
VK_ANSI_N = 0x2D,
VK_ANSI_M = 0x2E,
VK_ANSI_Period = 0x2F,
VK_ANSI_Grave = 0x32,
VK_ANSI_KeypadDecimal = 0x41,
VK_ANSI_KeypadMultiply = 0x43,
VK_ANSI_KeypadPlus = 0x45,
VK_ANSI_KeypadClear = 0x47,
VK_ANSI_KeypadDivide = 0x4B,
VK_ANSI_KeypadEnter = 0x4C,
VK_ANSI_KeypadMinus = 0x4E,
VK_ANSI_KeypadEquals = 0x51,
VK_ANSI_Keypad0 = 0x52,
VK_ANSI_Keypad1 = 0x53,
VK_ANSI_Keypad2 = 0x54,
VK_ANSI_Keypad3 = 0x55,
VK_ANSI_Keypad4 = 0x56,
VK_ANSI_Keypad5 = 0x57,
VK_ANSI_Keypad6 = 0x58,
VK_ANSI_Keypad7 = 0x59,
VK_ANSI_Keypad8 = 0x5B,
VK_ANSI_Keypad9 = 0x5C
};
/* keycodes for keys that are independent of keyboard layout*/
enum: uint16_t {
VK_Return = 0x24,
VK_Tab = 0x30,
VK_Space = 0x31,
VK_Delete = 0x33,
VK_Escape = 0x35,
VK_Command = 0x37,
VK_Shift = 0x38,
VK_CapsLock = 0x39,
VK_Option = 0x3A,
VK_Control = 0x3B,
VK_RightShift = 0x3C,
VK_RightOption = 0x3D,
VK_RightControl = 0x3E,
VK_Function = 0x3F,
VK_F17 = 0x40,
VK_VolumeUp = 0x48,
VK_VolumeDown = 0x49,
VK_Mute = 0x4A,
VK_F18 = 0x4F,
VK_F19 = 0x50,
VK_F20 = 0x5A,
VK_F5 = 0x60,
VK_F6 = 0x61,
VK_F7 = 0x62,
VK_F3 = 0x63,
VK_F8 = 0x64,
VK_F9 = 0x65,
VK_F11 = 0x67,
VK_F13 = 0x69,
VK_F16 = 0x6A,
VK_F14 = 0x6B,
VK_F10 = 0x6D,
VK_F12 = 0x6F,
VK_F15 = 0x71,
VK_Help = 0x72,
VK_Home = 0x73,
VK_PageUp = 0x74,
VK_ForwardDelete = 0x75,
VK_F4 = 0x76,
VK_End = 0x77,
VK_F2 = 0x78,
VK_PageDown = 0x79,
VK_F1 = 0x7A,
VK_LeftArrow = 0x7B,
VK_RightArrow = 0x7C,
VK_DownArrow = 0x7D,
VK_UpArrow = 0x7E
};
#endif /* KeyCodes_h */

View File

@ -83,7 +83,7 @@ class MachineJamHandler: public CPU6502::AllRAMProcessor::JamHandler {
- (instancetype)init {
self = [super init];
if (self) {
if(self) {
_cppJamHandler = new MachineJamHandler(self);
_processor.set_jam_handler(_cppJamHandler);
}

View File

@ -1,463 +0,0 @@
//
// CRT.cpp
// Clock Signal
//
// Created by Thomas Harte on 19/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#include "CRT.hpp"
#include <stdarg.h>
#include <math.h>
using namespace Outputs;
static const uint32_t kCRTFixedPointRange = 0xf7ffffff;
static const uint32_t kCRTFixedPointOffset = 0x04000000;
#define kRetraceXMask 0x01
#define kRetraceYMask 0x02
void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display)
{
const unsigned int syncCapacityLineChargeThreshold = 3;
const unsigned int millisecondsHorizontalRetraceTime = 7; // source: Dictionary of Video and Television Technology, p. 234
const unsigned int scanlinesVerticalRetraceTime = 10; // source: ibid
// To quote:
//
// "retrace interval; The interval of time for the return of the blanked scanning beam of
// a TV picture tube or camera tube to the starting point of a line or field. It is about 7 µs
// for horizontal retrace and 500 to 750 µs for vertical retrace in NTSC and PAL TV."
_time_multiplier = (1000 + cycles_per_line - 1) / cycles_per_line;
height_of_display += (height_of_display / 20); // this is the overrun area we'll use to
// store fundamental display configuration properties
_height_of_display = height_of_display + 5;
_cycles_per_line = cycles_per_line * _time_multiplier;
// generate timing values implied by the given arbuments
_hsync_error_window = _cycles_per_line >> 5;
_sync_capacitor_charge_threshold = ((syncCapacityLineChargeThreshold * _cycles_per_line) * 50) >> 7;
_horizontal_retrace_time = (millisecondsHorizontalRetraceTime * _cycles_per_line) >> 6;
const unsigned int vertical_retrace_time = scanlinesVerticalRetraceTime * _cycles_per_line;
const float halfLineWidth = (float)_height_of_display * 2.0f;
for(int c = 0; c < 4; c++)
{
_scanSpeed[c].x = (c&kRetraceXMask) ? -(kCRTFixedPointRange / _horizontal_retrace_time) : (kCRTFixedPointRange / _cycles_per_line);
_scanSpeed[c].y = (c&kRetraceYMask) ? -(kCRTFixedPointRange / vertical_retrace_time) : (kCRTFixedPointRange / (_height_of_display * _cycles_per_line));
// width should be 1.0 / _height_of_display, rotated to match the direction
float angle = atan2f(_scanSpeed[c].y, _scanSpeed[c].x);
_beamWidth[c].x = (uint32_t)((sinf(angle) / halfLineWidth) * kCRTFixedPointRange);
_beamWidth[c].y = (uint32_t)((cosf(angle) / halfLineWidth) * kCRTFixedPointRange);
}
}
CRT::CRT(unsigned int cycles_per_line, unsigned int height_of_display, unsigned int number_of_buffers, ...)
{
set_new_timing(cycles_per_line, height_of_display);
// generate buffers for signal storage as requested — format is
// number of buffers, size of buffer 1, size of buffer 2...
const uint16_t bufferWidth = 2048;
const uint16_t bufferHeight = 2048;
for(int frame = 0; frame < sizeof(_frame_builders) / sizeof(*_frame_builders); frame++)
{
va_list va;
va_start(va, number_of_buffers);
_frame_builders[frame] = new CRTFrameBuilder(bufferWidth, bufferHeight, number_of_buffers, va);
va_end(va);
}
_frames_with_delegate = 0;
_frame_read_pointer = 0;
_current_frame_builder = _frame_builders[0];
// reset raster position
_rasterPosition.x = _rasterPosition.y = 0;
// reset flywheel sync
_expected_next_hsync = _cycles_per_line;
_horizontal_counter = 0;
// reset the vertical charge capacitor
_sync_capacitor_charge_level = 0;
// start off not in horizontal sync, not receiving a sync signal
_is_receiving_sync = false;
_is_in_hsync = false;
_is_in_vsync = false;
}
CRT::~CRT()
{
for(int frame = 0; frame < sizeof(_frame_builders) / sizeof(*_frame_builders); frame++)
{
delete _frame_builders[frame];
}
}
#pragma mark - Sync loop
CRT::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
SyncEvent proposedEvent = SyncEvent::None;
unsigned int proposedSyncTime = cycles_to_run_for;
// will an acceptable vertical sync be triggered?
if (vsync_is_requested && !_is_in_vsync) {
if (_sync_capacitor_charge_level >= _sync_capacitor_charge_threshold && _rasterPosition.y >= 3*(kCRTFixedPointRange >> 2)) {
proposedSyncTime = 0;
proposedEvent = SyncEvent::StartVSync;
_did_detect_vsync = true;
}
}
// have we overrun the maximum permitted number of horizontal syncs for this frame?
if (!_is_in_vsync) {
unsigned int time_until_end_of_frame = (kCRTFixedPointRange - _rasterPosition.y) / _scanSpeed[0].y;
if(time_until_end_of_frame < proposedSyncTime) {
proposedSyncTime = time_until_end_of_frame;
proposedEvent = SyncEvent::StartVSync;
}
} else {
unsigned int time_until_start_of_frame = _rasterPosition.y / (uint32_t)(-_scanSpeed[kRetraceYMask].y);
if(time_until_start_of_frame < proposedSyncTime) {
proposedSyncTime = time_until_start_of_frame;
proposedEvent = SyncEvent::EndVSync;
}
}
*cycles_advanced = proposedSyncTime;
return proposedEvent;
}
CRT::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
// do we recognise this hsync, thereby adjusting future time expectations?
if(hsync_is_requested) {
if (_horizontal_counter < _hsync_error_window || _horizontal_counter >= _expected_next_hsync - _hsync_error_window) {
_did_detect_hsync = true;
unsigned int time_now = (_horizontal_counter < _hsync_error_window) ? _expected_next_hsync + _horizontal_counter : _horizontal_counter;
_expected_next_hsync = (_expected_next_hsync + _expected_next_hsync + _expected_next_hsync + time_now) >> 2;
}
}
SyncEvent proposedEvent = SyncEvent::None;
unsigned int proposedSyncTime = cycles_to_run_for;
// will we end an ongoing hsync?
if (_horizontal_counter < _horizontal_retrace_time && _horizontal_counter+proposedSyncTime >= _horizontal_retrace_time) {
proposedSyncTime = _horizontal_retrace_time - _horizontal_counter;
proposedEvent = SyncEvent::EndHSync;
}
// will we start an hsync?
if (_horizontal_counter + proposedSyncTime >= _expected_next_hsync) {
proposedSyncTime = _expected_next_hsync - _horizontal_counter;
proposedEvent = SyncEvent::StartHSync;
}
*cycles_advanced = proposedSyncTime;
return proposedEvent;
}
void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Type type)
{
number_of_cycles *= _time_multiplier;
bool is_output_run = ((type == Type::Level) || (type == Type::Data));
uint16_t tex_x = 0;
uint16_t tex_y = 0;
if(is_output_run && _current_frame_builder) {
tex_x = _current_frame_builder->_write_x_position;
tex_y = _current_frame_builder->_write_y_position;
}
while(number_of_cycles) {
unsigned int time_until_vertical_sync_event, time_until_horizontal_sync_event;
SyncEvent next_vertical_sync_event = this->get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event);
SyncEvent next_horizontal_sync_event = this->get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
// get the next sync event and its timing; hsync request is instantaneous (being edge triggered) so
// set it to false for the next run through this loop (if any)
unsigned int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
hsync_requested = false;
vsync_requested = false;
uint8_t *next_run = (is_output_run && _current_frame_builder && next_run_length) ? _current_frame_builder->get_next_run() : nullptr;
int lengthMask = (_is_in_hsync ? kRetraceXMask : 0) | (_is_in_vsync ? kRetraceYMask : 0);
#define position_x(v) (*(uint16_t *)&next_run[kCRTSizeOfVertex*v + kCRTVertexOffsetOfPosition + 0])
#define position_y(v) (*(uint16_t *)&next_run[kCRTSizeOfVertex*v + kCRTVertexOffsetOfPosition + 2])
#define tex_x(v) (*(uint16_t *)&next_run[kCRTSizeOfVertex*v + kCRTVertexOffsetOfTexCoord + 0])
#define tex_y(v) (*(uint16_t *)&next_run[kCRTSizeOfVertex*v + kCRTVertexOffsetOfTexCoord + 2])
#define lateral(v) next_run[kCRTSizeOfVertex*v + kCRTVertexOffsetOfLateral]
#define InternalToUInt16(v) ((v) + 32768) >> 16
if(next_run)
{
// set the type, initial raster position and type of this run
position_x(0) = position_x(4) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.x + _beamWidth[lengthMask].x);
position_y(0) = position_y(4) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.y + _beamWidth[lengthMask].y);
position_x(1) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.x - _beamWidth[lengthMask].x);
position_y(1) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.y - _beamWidth[lengthMask].y);
tex_x(0) = tex_x(1) = tex_x(4) = tex_x;
// these things are constants across the line so just throw them out now
tex_y(0) = tex_y(4) = tex_y(1) = tex_y(2) = tex_y(3) = tex_y(5) = tex_y;
lateral(0) = lateral(4) = lateral(5) = 0;
lateral(1) = lateral(2) = lateral(3) = 1;
}
// advance the raster position as dictated by current sync status
int64_t end_position[2];
end_position[0] = (int64_t)_rasterPosition.x + (int64_t)next_run_length * (int32_t)_scanSpeed[lengthMask].x;
end_position[1] = (int64_t)_rasterPosition.y + (int64_t)next_run_length * (int32_t)_scanSpeed[lengthMask].y;
if (_is_in_hsync)
_rasterPosition.x = (uint32_t)std::max((int64_t)0, end_position[0]);
else
_rasterPosition.x = (uint32_t)std::min((int64_t)kCRTFixedPointRange, end_position[0]);
if (_is_in_vsync)
_rasterPosition.y = (uint32_t)std::max((int64_t)0, end_position[1]);
else
_rasterPosition.y = (uint32_t)std::min((int64_t)kCRTFixedPointRange, end_position[1]);
if(next_run)
{
// store the final raster position
position_x(2) = position_x(3) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.x - _beamWidth[lengthMask].x);
position_y(2) = position_y(3) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.y - _beamWidth[lengthMask].y);
position_x(5) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.x + _beamWidth[lengthMask].x);
position_y(5) = InternalToUInt16(kCRTFixedPointOffset + _rasterPosition.y + _beamWidth[lengthMask].y);
// if this is a data run then advance the buffer pointer
if(type == Type::Data) tex_x += next_run_length / _time_multiplier;
// if this is a data or level run then store the end point
tex_x(2) = tex_x(3) = tex_x(5) = tex_x;
}
// decrement the number of cycles left to run for and increment the
// horizontal counter appropriately
number_of_cycles -= next_run_length;
_horizontal_counter += next_run_length;
// either charge or deplete the vertical retrace capacitor (making sure it stops at 0)
if (vsync_charging && !_is_in_vsync)
_sync_capacitor_charge_level += next_run_length;
else
_sync_capacitor_charge_level = std::max(_sync_capacitor_charge_level - (int)next_run_length, 0);
// react to the incoming event...
if(next_run_length == time_until_horizontal_sync_event)
{
switch(next_horizontal_sync_event)
{
// start of hsync: zero the scanline counter, note that we're now in
// horizontal sync, increment the lines-in-this-frame counter
case SyncEvent::StartHSync:
_horizontal_counter = 0;
_is_in_hsync = true;
break;
// end of horizontal sync: update the flywheel's velocity, note that we're no longer
// in horizontal sync
case SyncEvent::EndHSync:
if (!_did_detect_hsync) {
_expected_next_hsync = (_expected_next_hsync + (_hsync_error_window >> 1) + _cycles_per_line) >> 1;
}
_did_detect_hsync = false;
_is_in_hsync = false;
break;
default: break;
}
}
if(next_run_length == time_until_vertical_sync_event)
{
switch(next_vertical_sync_event)
{
// start of vertical sync: reset the lines-in-this-frame counter,
// load the retrace counter with the amount of time it'll take to retrace
case SyncEvent::StartVSync:
_is_in_vsync = true;
_sync_capacitor_charge_level = 0;
break;
// end of vertical sync: tell the delegate that we finished vertical sync,
// releasing all runs back into the common pool
case SyncEvent::EndVSync:
if(_delegate && _current_frame_builder)
{
_current_frame_builder->complete();
_frames_with_delegate++;
_delegate->crt_did_end_frame(this, &_current_frame_builder->frame, _did_detect_vsync);
}
if(_frames_with_delegate < kCRTNumberOfFrames)
{
_frame_read_pointer = (_frame_read_pointer + 1)%kCRTNumberOfFrames;
_current_frame_builder = _frame_builders[_frame_read_pointer];
_current_frame_builder->reset();
}
else
_current_frame_builder = nullptr;
_is_in_vsync = false;
_did_detect_vsync = false;
break;
default: break;
}
}
}
}
void CRT::return_frame()
{
_frames_with_delegate--;
}
#pragma mark - delegate
void CRT::set_delegate(CRTDelegate *delegate)
{
_delegate = delegate;
}
#pragma mark - stream feeding methods
/*
These all merely channel into advance_cycles, supplying appropriate arguments
*/
void CRT::output_sync(unsigned int number_of_cycles)
{
bool _hsync_requested = !_is_receiving_sync; // ensure this really is edge triggered; someone calling output_sync twice in succession shouldn't trigger it twice
_is_receiving_sync = true;
advance_cycles(number_of_cycles, _hsync_requested, false, true, Type::Sync);
}
void CRT::output_blank(unsigned int number_of_cycles)
{
bool _vsync_requested = _is_receiving_sync;
_is_receiving_sync = false;
advance_cycles(number_of_cycles, false, _vsync_requested, false, Type::Blank);
}
void CRT::output_level(unsigned int number_of_cycles)
{
bool _vsync_requested = _is_receiving_sync;
_is_receiving_sync = false;
advance_cycles(number_of_cycles, false, _vsync_requested, false, Type::Level);
}
void CRT::output_data(unsigned int number_of_cycles)
{
bool _vsync_requested = _is_receiving_sync;
_is_receiving_sync = false;
advance_cycles(number_of_cycles, false, _vsync_requested, false, Type::Data);
}
#pragma mark - Buffer supply
void CRT::allocate_write_area(int required_length)
{
if(_current_frame_builder) _current_frame_builder->allocate_write_area(required_length);
}
uint8_t *CRT::get_write_target_for_buffer(int buffer)
{
if (!_current_frame_builder) return nullptr;
return _current_frame_builder->get_write_target_for_buffer(buffer);
}
#pragma mark - CRTFrame
CRTFrameBuilder::CRTFrameBuilder(uint16_t width, uint16_t height, unsigned int number_of_buffers, va_list buffer_sizes)
{
frame.size.width = width;
frame.size.height = height;
frame.number_of_buffers = number_of_buffers;
frame.buffers = new CRTBuffer[number_of_buffers];
for(int buffer = 0; buffer < number_of_buffers; buffer++)
{
frame.buffers[buffer].depth = va_arg(buffer_sizes, unsigned int);
frame.buffers[buffer].data = new uint8_t[width * height * frame.buffers[buffer].depth];
}
reset();
}
CRTFrameBuilder::~CRTFrameBuilder()
{
for(int buffer = 0; buffer < frame.number_of_buffers; buffer++)
delete[] frame.buffers[buffer].data;
delete frame.buffers;
}
void CRTFrameBuilder::reset()
{
frame.number_of_runs = 0;
_next_write_x_position = _next_write_y_position = 0;
frame.dirty_size.width = 0;
frame.dirty_size.height = 1;
}
void CRTFrameBuilder::complete()
{
frame.runs = &_all_runs[0];
}
uint8_t *CRTFrameBuilder::get_next_run()
{
const size_t vertices_per_run = 6;
const size_t size_of_run = kCRTSizeOfVertex * vertices_per_run;
// get a run from the allocated list, allocating more if we're about to overrun
if(frame.number_of_runs * size_of_run >= _all_runs.size())
{
_all_runs.resize(_all_runs.size() + size_of_run * 200);
}
uint8_t *next_run = &_all_runs[frame.number_of_runs * size_of_run];
frame.number_of_runs++;
return next_run;
}
void CRTFrameBuilder::allocate_write_area(int required_length)
{
if (_next_write_x_position + required_length > frame.size.width)
{
_next_write_x_position = 0;
_next_write_y_position = (_next_write_y_position+1)&(frame.size.height-1);
frame.dirty_size.height++;
}
_write_x_position = _next_write_x_position;
_write_y_position = _next_write_y_position;
_write_target_pointer = (_write_y_position * frame.size.width) + _write_x_position;
_next_write_x_position += required_length;
frame.dirty_size.width = std::max(frame.dirty_size.width, _next_write_x_position);
}
uint8_t *CRTFrameBuilder::get_write_target_for_buffer(int buffer)
{
return &frame.buffers[buffer].data[_write_target_pointer * frame.buffers[buffer].depth];
}

View File

@ -1,129 +0,0 @@
//
// CRT.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#ifndef CRT_cpp
#define CRT_cpp
#include <stdint.h>
#include <stdarg.h>
#include <string>
#include <vector>
#include "CRTFrame.h"
namespace Outputs {
class CRT;
struct CRTFrameBuilder {
CRTFrame frame;
CRTFrameBuilder(uint16_t width, uint16_t height, unsigned int number_of_buffers, va_list buffer_sizes);
~CRTFrameBuilder();
private:
std::vector<uint8_t> _all_runs;
void reset();
void complete();
uint8_t *get_next_run();
friend CRT;
void allocate_write_area(int required_length);
uint8_t *get_write_target_for_buffer(int buffer);
// a pointer to the section of content buffer currently being
// returned and to where the next section will begin
uint16_t _next_write_x_position, _next_write_y_position;
uint16_t _write_x_position, _write_y_position;
size_t _write_target_pointer;
};
static const int kCRTNumberOfFrames = 4;
class CRT {
public:
CRT(unsigned int cycles_per_line, unsigned int height_of_display, unsigned int number_of_buffers, ...);
~CRT();
void set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display);
void output_sync(unsigned int number_of_cycles);
void output_blank(unsigned int number_of_cycles);
void output_level(unsigned int number_of_cycles);
void output_data(unsigned int number_of_cycles);
class CRTDelegate {
public:
virtual void crt_did_end_frame(CRT *crt, CRTFrame *frame, bool did_detect_vsync) = 0;
};
void set_delegate(CRTDelegate *delegate);
void return_frame();
void allocate_write_area(int required_length);
uint8_t *get_write_target_for_buffer(int buffer);
private:
// the incoming clock lengths will be multiplied by something to give at least 1000
// sample points per line
unsigned int _time_multiplier;
// fundamental creator-specified properties
unsigned int _cycles_per_line;
unsigned int _height_of_display;
// properties directly derived from there
unsigned int _hsync_error_window; // the permitted window around the expected sync position in which a sync pulse will be recognised; calculated once at init
// the current scanning position
struct Vector {
uint32_t x, y;
} _rasterPosition, _scanSpeed[4], _beamWidth[4];
// the run delegate and the triple buffer
CRTFrameBuilder *_frame_builders[kCRTNumberOfFrames];
CRTFrameBuilder *_current_frame_builder;
int _frames_with_delegate;
int _frame_read_pointer;
CRTDelegate *_delegate;
// outer elements of sync separation
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
bool _did_detect_hsync; // true if horizontal sync was detected during this scanline (so, this affects flywheel adjustments)
int _sync_capacitor_charge_level; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
int _sync_capacitor_charge_threshold; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
int _is_in_vsync;
// components of the flywheel sync
unsigned int _horizontal_counter; // time run since the _start_ of the last horizontal sync
unsigned int _expected_next_hsync; // our current expection of when the next horizontal sync will be encountered (which implies current flywheel velocity)
unsigned int _horizontal_retrace_time;
bool _is_in_hsync; // true for the duration of a horizontal sync — used to determine beam running direction and speed
bool _did_detect_vsync; // true if vertical sync was detected in the input stream rather than forced by emergency measure
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
enum Type {
Sync, Level, Data, Blank
} type;
void advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, bool vsync_charging, Type type);
// the inner entry point that determines whether and when the next sync event will occur within
// the current output window
enum SyncEvent {
None,
StartHSync, EndHSync,
StartVSync, EndVSync
};
SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
};
}
#endif /* CRT_cpp */

407
Outputs/CRT/CRT.cpp Normal file
View File

@ -0,0 +1,407 @@
//
// CRT.cpp
// Clock Signal
//
// Created by Thomas Harte on 19/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#include "CRT.hpp"
#include "CRTOpenGL.hpp"
#include <stdarg.h>
#include <math.h>
using namespace Outputs::CRT;
void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator)
{
_openGL_output_builder->set_colour_format(colour_space, colour_cycle_numerator, colour_cycle_denominator);
const unsigned int syncCapacityLineChargeThreshold = 2;
const unsigned int millisecondsHorizontalRetraceTime = 7; // source: Dictionary of Video and Television Technology, p. 234
const unsigned int scanlinesVerticalRetraceTime = 10; // source: ibid
// To quote:
//
// "retrace interval; The interval of time for the return of the blanked scanning beam of
// a TV picture tube or camera tube to the starting point of a line or field. It is about 7 µs
// for horizontal retrace and 500 to 750 µs for vertical retrace in NTSC and PAL TV."
_time_multiplier = (2000 + cycles_per_line - 1) / cycles_per_line;
// store fundamental display configuration properties
_height_of_display = height_of_display;
_cycles_per_line = cycles_per_line * _time_multiplier;
// generate timing values implied by the given arbuments
_sync_capacitor_charge_threshold = (int)(syncCapacityLineChargeThreshold * _cycles_per_line);
// create the two flywheels
_horizontal_flywheel = std::unique_ptr<Flywheel>(new Flywheel(_cycles_per_line, (millisecondsHorizontalRetraceTime * _cycles_per_line) >> 6));
_vertical_flywheel = std::unique_ptr<Flywheel>(new Flywheel(_cycles_per_line * height_of_display, scanlinesVerticalRetraceTime * _cycles_per_line));
// figure out the divisor necessary to get the horizontal flywheel into a 16-bit range
unsigned int real_clock_scan_period = (_cycles_per_line * height_of_display) / (_time_multiplier * _common_output_divisor);
_vertical_flywheel_output_divider = (uint16_t)(ceilf(real_clock_scan_period / 65536.0f) * (_time_multiplier * _common_output_divisor));
_openGL_output_builder->set_timing(_cycles_per_line, _height_of_display, _horizontal_flywheel->get_scan_period(), _vertical_flywheel->get_scan_period(), _vertical_flywheel_output_divider);
}
void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType displayType)
{
switch(displayType)
{
case DisplayType::PAL50:
set_new_timing(cycles_per_line, 312, ColourSpace::YUV, 1135, 4);
break;
case DisplayType::NTSC60:
set_new_timing(cycles_per_line, 262, ColourSpace::YIQ, 545, 2);
break;
}
}
CRT::CRT(unsigned int common_output_divisor) :
_sync_capacitor_charge_level(0),
_is_receiving_sync(false),
_sync_period(0),
_common_output_divisor(common_output_divisor),
_is_writing_composite_run(false),
_delegate(nullptr),
_frames_since_last_delegate_call(0) {}
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int buffer_depth) : CRT(common_output_divisor)
{
_openGL_output_builder = std::unique_ptr<OpenGLOutputBuilder>(new OpenGLOutputBuilder(buffer_depth));
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator);
}
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, DisplayType displayType, unsigned int buffer_depth) : CRT(common_output_divisor)
{
_openGL_output_builder = std::unique_ptr<OpenGLOutputBuilder>(new OpenGLOutputBuilder(buffer_depth));
set_new_display_type(cycles_per_line, displayType);
}
#pragma mark - Sync loop
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
return _vertical_flywheel->get_next_event_in_period(vsync_is_requested, cycles_to_run_for, cycles_advanced);
}
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
return _horizontal_flywheel->get_next_event_in_period(hsync_is_requested, cycles_to_run_for, cycles_advanced);
}
#define output_position_x(v) (*(uint16_t *)&next_run[OutputVertexSize*v + OutputVertexOffsetOfPosition + 0])
#define output_position_y(v) (*(uint16_t *)&next_run[OutputVertexSize*v + OutputVertexOffsetOfPosition + 2])
#define output_tex_x(v) (*(uint16_t *)&next_run[OutputVertexSize*v + OutputVertexOffsetOfTexCoord + 0])
#define output_tex_y(v) (*(uint16_t *)&next_run[OutputVertexSize*v + OutputVertexOffsetOfTexCoord + 2])
#define output_lateral(v) next_run[OutputVertexSize*v + OutputVertexOffsetOfLateral]
#define output_frame_id(v) next_run[OutputVertexSize*v + OutputVertexOffsetOfFrameID]
#define output_timestamp(v) (*(uint32_t *)&next_run[OutputVertexSize*v + OutputVertexOffsetOfTimestamp])
#define source_input_position_x(v) (*(uint16_t *)&next_run[SourceVertexSize*v + SourceVertexOffsetOfInputPosition + 0])
#define source_input_position_y(v) (*(uint16_t *)&next_run[SourceVertexSize*v + SourceVertexOffsetOfInputPosition + 2])
#define source_output_position_x(v) (*(uint16_t *)&next_run[SourceVertexSize*v + SourceVertexOffsetOfOutputPosition + 0])
#define source_output_position_y(v) (*(uint16_t *)&next_run[SourceVertexSize*v + SourceVertexOffsetOfOutputPosition + 2])
#define source_phase(v) next_run[SourceVertexSize*v + SourceVertexOffsetOfPhaseAmplitudeAndOffset + 0]
#define source_amplitude(v) next_run[SourceVertexSize*v + SourceVertexOffsetOfPhaseAmplitudeAndOffset + 1]
#define source_offset(v) next_run[SourceVertexSize*v + SourceVertexOffsetOfPhaseAmplitudeAndOffset + 2]
#define source_phase_time(v) (*(uint16_t *)&next_run[SourceVertexSize*v + SourceVertexOffsetOfPhaseTime])
void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type, uint16_t tex_x, uint16_t tex_y)
{
number_of_cycles *= _time_multiplier;
bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data));
while(number_of_cycles) {
unsigned int time_until_vertical_sync_event, time_until_horizontal_sync_event;
Flywheel::SyncEvent next_vertical_sync_event = get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event);
Flywheel::SyncEvent next_horizontal_sync_event = get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
// get the next sync event and its timing; hsync request is instantaneous (being edge triggered) so
// set it to false for the next run through this loop (if any)
unsigned int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
hsync_requested = false;
vsync_requested = false;
bool is_output_segment = ((is_output_run && next_run_length) && !_horizontal_flywheel->is_in_retrace() && !_vertical_flywheel->is_in_retrace());
uint8_t *next_run = nullptr;
if(is_output_segment)
{
next_run = (_openGL_output_builder->get_output_device() == Monitor) ? _openGL_output_builder->get_next_output_run() : _openGL_output_builder->get_next_source_run();
}
// Vertex output is arranged for triangle strips, as:
//
// 2 [4/5]
//
// [0/1] 3
if(next_run)
{
if(_openGL_output_builder->get_output_device() == Monitor)
{
// set the type, initial raster position and type of this run
output_position_x(0) = output_position_x(1) = output_position_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
output_position_y(0) = output_position_y(1) = output_position_y(2) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _openGL_output_builder->get_current_field_time();
output_tex_x(0) = output_tex_x(1) = output_tex_x(2) = tex_x;
// these things are constants across the line so just throw them out now
output_tex_y(0) = output_tex_y(1) = output_tex_y(2) = output_tex_y(3) = output_tex_y(4) = output_tex_y(5) = tex_y;
output_lateral(0) = output_lateral(1) = output_lateral(3) = 0;
output_lateral(2) = output_lateral(4) = output_lateral(5) = 1;
output_frame_id(0) = output_frame_id(1) = output_frame_id(2) = output_frame_id(3) = output_frame_id(4) = output_frame_id(5) = (uint8_t)_openGL_output_builder->get_current_field();
}
else
{
source_input_position_x(0) = tex_x;
source_input_position_y(0) = source_input_position_y(1) = tex_y;
source_output_position_x(0) = (uint16_t)_horizontal_flywheel->get_current_output_position();
source_output_position_y(0) = source_output_position_y(1) = _openGL_output_builder->get_composite_output_y();
source_phase(0) = source_phase(1) = _colour_burst_phase;
source_amplitude(0) = source_amplitude(1) = _colour_burst_amplitude;
source_phase_time(0) = source_phase_time(1) = _colour_burst_time;
source_offset(0) = 0;
source_offset(1) = 255;
}
}
// decrement the number of cycles left to run for and increment the
// horizontal counter appropriately
number_of_cycles -= next_run_length;
_openGL_output_builder->add_to_field_time(next_run_length);
// either charge or deplete the vertical retrace capacitor (making sure it stops at 0)
if(vsync_charging)
_sync_capacitor_charge_level += next_run_length;
else
_sync_capacitor_charge_level = std::max(_sync_capacitor_charge_level - (int)next_run_length, 0);
// react to the incoming event...
_horizontal_flywheel->apply_event(next_run_length, (next_run_length == time_until_horizontal_sync_event) ? next_horizontal_sync_event : Flywheel::SyncEvent::None);
_vertical_flywheel->apply_event(next_run_length, (next_run_length == time_until_vertical_sync_event) ? next_vertical_sync_event : Flywheel::SyncEvent::None);
if(next_run)
{
// if this is a data run then advance the buffer pointer
if(type == Scan::Type::Data && source_divider) tex_x += next_run_length / (_time_multiplier * source_divider);
if(_openGL_output_builder->get_output_device() == Monitor)
{
// store the final raster position
output_position_x(3) = output_position_x(4) = output_position_x(5) = (uint16_t)_horizontal_flywheel->get_current_output_position();
output_position_y(3) = output_position_y(4) = output_position_y(5) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
output_timestamp(3) = output_timestamp(4) = output_timestamp(5) = _openGL_output_builder->get_current_field_time();
output_tex_x(3) = output_tex_x(4) = output_tex_x(5) = tex_x;
_openGL_output_builder->complete_output_run(6);
}
else
{
source_input_position_x(1) = tex_x;
source_output_position_x(1) = (uint16_t)_horizontal_flywheel->get_current_output_position();
_openGL_output_builder->complete_source_run();
}
}
// if this is horizontal retrace then advance the output line counter and bookend an output run
if(_openGL_output_builder->get_output_device() == Television)
{
Flywheel::SyncEvent honoured_event = Flywheel::SyncEvent::None;
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event != Flywheel::SyncEvent::None) honoured_event = next_vertical_sync_event;
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event != Flywheel::SyncEvent::None) honoured_event = next_horizontal_sync_event;
bool needs_endpoint =
(honoured_event == Flywheel::SyncEvent::StartRetrace && _is_writing_composite_run) ||
(honoured_event == Flywheel::SyncEvent::EndRetrace && !_horizontal_flywheel->is_in_retrace() && !_vertical_flywheel->is_in_retrace());
if(needs_endpoint)
{
uint8_t *next_run = _openGL_output_builder->get_next_output_run();
output_position_x(0) = output_position_x(1) = output_position_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
output_position_y(0) = output_position_y(1) = output_position_y(2) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _openGL_output_builder->get_current_field_time();
output_tex_x(0) = output_tex_x(1) = output_tex_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
output_tex_y(0) = output_tex_y(1) = output_tex_y(2) = _openGL_output_builder->get_composite_output_y();
output_lateral(0) = 0;
output_lateral(1) = _is_writing_composite_run ? 1 : 0;
output_lateral(2) = 1;
output_frame_id(0) = output_frame_id(1) = output_frame_id(2) = (uint8_t)_openGL_output_builder->get_current_field();
_openGL_output_builder->complete_output_run(3);
_is_writing_composite_run ^= true;
}
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace)
{
_openGL_output_builder->increment_composite_output_y();
}
}
// if this is vertical retrace then adcance a field
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event == Flywheel::SyncEvent::EndRetrace)
{
if(_delegate)
{
_frames_since_last_delegate_call++;
if(_frames_since_last_delegate_call == 100)
{
_delegate->crt_did_end_batch_of_frames(this, _frames_since_last_delegate_call, _vertical_flywheel->get_and_reset_number_of_surprises());
_frames_since_last_delegate_call = 0;
}
}
_openGL_output_builder->increment_field();
}
}
}
#undef output_position_x
#undef output_position_y
#undef output_tex_x
#undef output_tex_y
#undef output_lateral
#undef output_timestamp
#undef input_input_position_x
#undef input_input_position_y
#undef input_output_position_x
#undef input_output_position_y
#undef input_phase
#undef input_amplitude
#undef input_phase_age
#pragma mark - stream feeding methods
void CRT::output_scan(const Scan *const scan)
{
const bool this_is_sync = (scan->type == Scan::Type::Sync);
const bool is_trailing_edge = (_is_receiving_sync && !this_is_sync);
const bool is_leading_edge = (!_is_receiving_sync && this_is_sync);
_is_receiving_sync = this_is_sync;
// This introduces a blackout period close to the expected vertical sync point in which horizontal syncs are not
// recognised, effectively causing the horizontal flywheel to freewheel during that period. This attempts to seek
// the problem that vertical sync otherwise often starts halfway through a scanline, which confuses the horizontal
// flywheel. I'm currently unclear whether this is an accurate solution to this problem.
const bool hsync_requested = is_leading_edge && !_vertical_flywheel->is_near_expected_sync();
const bool vsync_requested = is_trailing_edge && (_sync_capacitor_charge_level >= _sync_capacitor_charge_threshold);
// simplified colour burst logic: if it's within the back porch we'll take it
if(scan->type == Scan::Type::ColourBurst)
{
if(_horizontal_flywheel->get_current_time() < (_horizontal_flywheel->get_standard_period() * 12) >> 6)
{
_colour_burst_time = (uint16_t)_colour_burst_time;
_colour_burst_phase = scan->phase;
_colour_burst_amplitude = scan->amplitude;
}
}
// TODO: inspect raw data for potential colour burst if required
_sync_period = _is_receiving_sync ? (_sync_period + scan->number_of_cycles) : 0;
advance_cycles(scan->number_of_cycles, scan->source_divider, hsync_requested, vsync_requested, this_is_sync, scan->type, scan->tex_x, scan->tex_y);
}
/*
These all merely channel into advance_cycles, supplying appropriate arguments
*/
void CRT::output_sync(unsigned int number_of_cycles)
{
Scan scan{
.type = Scan::Type::Sync,
.number_of_cycles = number_of_cycles
};
output_scan(&scan);
}
void CRT::output_blank(unsigned int number_of_cycles)
{
Scan scan {
.type = Scan::Type::Blank,
.number_of_cycles = number_of_cycles
};
output_scan(&scan);
}
void CRT::output_level(unsigned int number_of_cycles)
{
Scan scan {
.type = Scan::Type::Level,
.number_of_cycles = number_of_cycles,
.tex_x = _openGL_output_builder->get_last_write_x_posiiton(),
.tex_y = _openGL_output_builder->get_last_write_y_posiiton()
};
output_scan(&scan);
}
void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude)
{
Scan scan {
.type = Scan::Type::ColourBurst,
.number_of_cycles = number_of_cycles,
.phase = phase,
.amplitude = amplitude
};
output_scan(&scan);
}
void CRT::output_data(unsigned int number_of_cycles, unsigned int source_divider)
{
_openGL_output_builder->reduce_previous_allocation_to(number_of_cycles / source_divider);
Scan scan {
.type = Scan::Type::Data,
.number_of_cycles = number_of_cycles,
.tex_x = _openGL_output_builder->get_last_write_x_posiiton(),
.tex_y = _openGL_output_builder->get_last_write_y_posiiton(),
.source_divider = source_divider
};
output_scan(&scan);
}
Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio)
{
first_cycle_after_sync *= _time_multiplier;
number_of_cycles *= _time_multiplier;
number_of_lines++;
// determine prima facie x extent
unsigned int horizontal_period = _horizontal_flywheel->get_standard_period();
unsigned int horizontal_scan_period = _horizontal_flywheel->get_scan_period();
unsigned int horizontal_retrace_period = horizontal_period - horizontal_scan_period;
float start_x = (float)((unsigned)first_cycle_after_sync - horizontal_retrace_period) / (float)horizontal_scan_period;
float width = (float)number_of_cycles / (float)horizontal_scan_period;
// determine prima facie y extent
unsigned int vertical_period = _vertical_flywheel->get_standard_period();
unsigned int vertical_scan_period = _vertical_flywheel->get_scan_period();
unsigned int vertical_retrace_period = vertical_period - vertical_scan_period;
float start_y = (float)(((unsigned)first_line_after_sync * horizontal_period) - vertical_retrace_period) / (float)vertical_scan_period;
float height = (float)((unsigned)number_of_lines * horizontal_period) / vertical_scan_period;
// adjust to ensure aspect ratio is correct
float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f);
float ideal_width = height * adjusted_aspect_ratio;
if(ideal_width > width)
{
start_x -= (ideal_width - width) * 0.5f;
width = ideal_width;
}
else
{
float ideal_height = width / adjusted_aspect_ratio;
start_y -= (ideal_height - height) * 0.5f;
height = ideal_height;
}
return Rect(start_x, start_y, width, height);
}

276
Outputs/CRT/CRT.hpp Normal file
View File

@ -0,0 +1,276 @@
//
// CRT.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#ifndef CRT_hpp
#define CRT_hpp
#include <stdint.h>
#include "CRTTypes.hpp"
#include "Internals/Flywheel.hpp"
#include "Internals/CRTOpenGL.hpp"
namespace Outputs {
namespace CRT {
class CRT;
class Delegate {
public:
virtual void crt_did_end_batch_of_frames(CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) = 0;
};
class CRT {
private:
CRT(unsigned int common_output_divisor);
// the incoming clock lengths will be multiplied by something to give at least 1000
// sample points per line
unsigned int _time_multiplier;
const unsigned int _common_output_divisor;
// fundamental creator-specified properties
unsigned int _cycles_per_line;
unsigned int _height_of_display;
// the two flywheels regulating scanning
std::unique_ptr<Flywheel> _horizontal_flywheel, _vertical_flywheel;
uint16_t _vertical_flywheel_output_divider;
// elements of sync separation
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
int _sync_capacitor_charge_level; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
int _sync_capacitor_charge_threshold; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
unsigned int _sync_period;
// each call to output_* generates a scan. A two-slot queue for scans allows edge extensions.
struct Scan {
enum Type {
Sync, Level, Data, Blank, ColourBurst
} type;
unsigned int number_of_cycles;
union {
struct {
unsigned int source_divider;
uint16_t tex_x, tex_y;
};
struct {
uint8_t phase, amplitude;
};
};
};
void output_scan(const Scan *scan);
uint8_t _colour_burst_phase, _colour_burst_amplitude;
uint16_t _colour_burst_time;
bool _is_writing_composite_run;
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
void advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type, uint16_t tex_x, uint16_t tex_y);
// the inner entry point that determines whether and when the next sync event will occur within
// the current output window
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
// OpenGL state, kept behind an opaque pointer to avoid inclusion of the GL headers here.
std::unique_ptr<OpenGLOutputBuilder> _openGL_output_builder;
// The delegate;
Delegate *_delegate;
unsigned int _frames_since_last_delegate_call;
public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
The requested number of buffers, each with the requested number of bytes per pixel,
is created for the machine to write raw pixel data to.
@param cycles_per_line The clock rate at which this CRT will be driven, specified as the number
of cycles expected to take up one whole scanline of the display.
@param common_output_divisor The greatest a priori common divisor of all cycle counts that will be
supplied to @c output_sync, @c output_data, etc; supply 1 if no greater divisor is known. For many
machines output will run at a fixed multiple of the clock rate; knowing this divisor can improve
internal precision.
@param height_of_dispaly The number of lines that nominally form one field of the display, rounded
up to the next whole integer.
@param colour_cycle_numerator Specifies the numerator for the per-line frequency of the colour subcarrier.
@param colour_cycle_denominator Specifies the denominator for the per-line frequency of the colour subcarrier.
The colour subcarrier is taken to have colour_cycle_numerator/colour_cycle_denominator cycles per line.
@param buffer_depth The depth per pixel of source data buffers to create for this machine. Machines
may provide per-clock-cycle data in the depth that they consider convenient, supplying a sampling
function to convert between their data format and either a composite or RGB signal, allowing that
work to be offloaded onto the GPU and allowing the output signal to be sampled at a rate appropriate
to the display size.
@see @c set_rgb_sampling_function , @c set_composite_sampling_function
*/
CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int buffer_depth);
/*! Constructs the CRT with the specified clock rate, with the display height and colour
subcarrier frequency dictated by a standard display type and with the requested number of
buffers, each with the requested number of bytes per pixel.
Exactly identical to calling the designated constructor with colour subcarrier information
looked up by display type.
*/
CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, DisplayType displayType, unsigned int buffer_depth);
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
been provided at construction. */
void set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator);
/*! Resets the CRT with new timing information derived from a new display type. The CRT then continues
as though the new timing had been provided at construction. */
void set_new_display_type(unsigned int cycles_per_line, DisplayType displayType);
/*! Output at the sync level.
@param number_of_cycles The amount of time to putput sync for.
*/
void output_sync(unsigned int number_of_cycles);
/*! Output at the blanking level.
@param number_of_cycles The amount of time to putput the blanking level for.
*/
void output_blank(unsigned int number_of_cycles);
/*! Outputs the first written to the most-recently created run of data repeatedly for a prolonged period.
@param number_of_cycles The number of cycles to repeat the output for.
*/
void output_level(unsigned int number_of_cycles);
/*! Declares that the caller has created a run of data via @c allocate_write_area and @c get_write_target_for_buffer
that is at least @c number_of_cycles long, and that the first @c number_of_cycles/source_divider should be spread
over that amount of time.
@param number_of_cycles The amount of data to output.
@param source_divider A divider for source data; if the divider is 1 then one source pixel is output every cycle,
if it is 2 then one source pixel covers two cycles; if it is n then one source pixel covers n cycles.
@see @c allocate_write_area , @c get_write_target_for_buffer
*/
void output_data(unsigned int number_of_cycles, unsigned int source_divider);
/*! Outputs a colour burst.
@param number_of_cycles The length of the colour burst.
@param phase The initial phase of the colour burst in a measuring system with 256 units
per circle, e.g. 0 = 0 degrees, 128 = 180 degrees, 256 = 360 degree.
@param amplitude The amplitude of the colour burst in 1/256ths of the amplitude of the
positive portion of the wave.
*/
void output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude);
/*! Ensures that the given number of output samples are allocated for writing.
The beginning of the most recently allocated area is used as the start
of data written by a call to @c output_data; it is acceptable to write and to
output less data than the amount requested but that may be less efficient.
@param required_length The number of samples to allocate.
@returns A pointer to the allocated area.
*/
inline uint8_t *allocate_write_area(size_t required_length)
{
return _openGL_output_builder->allocate_write_area(required_length);
}
/*! Causes appropriate OpenGL or OpenGL ES calls to be issued in order to draw the current CRT state.
The caller is responsible for ensuring that a valid OpenGL context exists for the duration of this call.
*/
inline void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
{
_openGL_output_builder->draw_frame(output_width, output_height, only_if_dirty);
}
/*! Tells the CRT that the next call to draw_frame will occur on a different OpenGL context than
the previous.
@param should_delete_resources If @c true then all resources textures, vertex arrays, etc
currently held by the CRT will be deleted now via calls to glDeleteTexture and equivalent. If
@c false then the references are simply marked as invalid.
*/
inline void set_openGL_context_will_change(bool should_delete_resources)
{
_openGL_output_builder->set_openGL_context_will_change(should_delete_resources);
}
/*! Sets a function that will map from whatever data the machine provided to a composite signal.
@param shader A GLSL fragment including a function with the signature
`float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)`
that evaluates to the composite signal level as a function of a source buffer, sampling location, colour
carrier phase and amplitude.
*/
inline void set_composite_sampling_function(const char *shader)
{
_openGL_output_builder->set_composite_sampling_function(shader);
}
/*! Sets a function that will map from whatever data the machine provided to an RGB signal.
If the output mode is composite then a default mapping from RGB to the display's composite
format will be applied.
@param shader A GLSL fragent including a function with the signature
`vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)` that evaluates to an RGB colour
as a function of:
* `usampler2D sampler` representing the source buffer;
* `vec2 coordinate` representing the source buffer location to sample from in the range [0, 1); and
* `vec2 icoordinate` representing the source buffer location to sample from as a pixel count, for easier multiple-pixels-per-byte unpacking.
*/
inline void set_rgb_sampling_function(const char *shader)
{
_openGL_output_builder->set_rgb_sampling_function(shader);
}
/*! Optionally sets a function that will map from an input cycle count to a colour carrier phase.
If this function is not supplied then the colour phase is determined from
the input clock rate and the the colour cycle clock rate. Machines whose per-line clock rate
is not intended exactly to match the normal line time may prefer to supply a custom function.
@param A GLSL fragent including a function with the signature
`float phase_for_clock_cycle(int cycle)` that returns the colour phase at the beginning of
the supplied cycle.
*/
// void set_phase_function(const char *shader);
inline void set_output_device(OutputDevice output_device)
{
_openGL_output_builder->set_output_device(output_device);
}
inline void set_visible_area(Rect visible_area)
{
_openGL_output_builder->set_visible_area(visible_area);
}
Rect get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio);
inline void set_delegate(Delegate *delegate)
{
_delegate = delegate;
}
};
}
}
#endif /* CRT_cpp */

47
Outputs/CRT/CRTTypes.hpp Normal file
View File

@ -0,0 +1,47 @@
//
// CRTTypes.hpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTTypes_h
#define CRTTypes_h
namespace Outputs {
namespace CRT {
struct Rect {
struct {
float x, y;
} origin;
struct {
float width, height;
} size;
Rect() {}
Rect(float x, float y, float width, float height) :
origin({.x = x, .y = y}), size({.width = width, .height = height}) {}
};
enum DisplayType {
PAL50,
NTSC60
};
enum ColourSpace {
YIQ,
YUV
};
enum OutputDevice {
Monitor,
Television
};
}
}
#endif /* CRTTypes_h */

View File

@ -0,0 +1,59 @@
//
// CRTContants.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/03/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTConstants_h
#define CRTConstants_h
#include "OpenGL.hpp"
#include <cstddef>
namespace Outputs {
namespace CRT {
// Output vertices are those used to copy from an input buffer — whether it describes data that maps directly to RGB
// or is one of the intermediate buffers that we've used to convert from composite towards RGB.
const GLsizei OutputVertexOffsetOfPosition = 0;
const GLsizei OutputVertexOffsetOfTexCoord = 4;
const GLsizei OutputVertexOffsetOfTimestamp = 8;
const GLsizei OutputVertexOffsetOfLateral = 12;
const GLsizei OutputVertexOffsetOfFrameID = 13;
const GLsizei OutputVertexSize = 16;
// Input vertices, used only in composite mode, map from the input buffer to temporary buffer locations; such
// remapping occurs to ensure a continous stream of data for each scan, giving correct out-of-bounds behaviour
const GLsizei SourceVertexOffsetOfInputPosition = 0;
const GLsizei SourceVertexOffsetOfOutputPosition = 4;
const GLsizei SourceVertexOffsetOfPhaseAmplitudeAndOffset = 8;
const GLsizei SourceVertexOffsetOfPhaseTime = 12;
const GLsizei SourceVertexSize = 16;
// These constants hold the size of the rolling buffer to which the CPU writes
const GLsizei InputBufferBuilderWidth = 2048;
const GLsizei InputBufferBuilderHeight = 1024;
// This is the size of the intermediate buffers used during composite to RGB conversion
const GLsizei IntermediateBufferWidth = 2048;
const GLsizei IntermediateBufferHeight = 2048;
// Some internal buffer sizes
const GLsizeiptr OutputVertexBufferDataSize = 262080; // a multiple of 6 * OutputVertexSize
const GLsizeiptr SourceVertexBufferDataSize = 87360; // a multiple of 2 * SourceVertexSize
// Runs are divided discretely by vertical syncs in order to put a usable bounds on the uniform used to track
// run age; that therefore creates a discrete number of fields that are stored. This number should be the
// number of historic fields that are required fully to complete a frame. It should be at least two and not
// more than four.
const int NumberOfFields = 4;
}
}
#endif /* CRTContants_h */

View File

@ -0,0 +1,57 @@
//
// CRTInputBufferBuilder.cpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "CRTInputBufferBuilder.hpp"
#include "CRTOpenGL.hpp"
#include <string.h>
using namespace Outputs::CRT;
CRTInputBufferBuilder::CRTInputBufferBuilder(size_t bytes_per_pixel) :
bytes_per_pixel(bytes_per_pixel),
_next_write_x_position(0),
_next_write_y_position(0),
last_uploaded_line(0),
_wraparound_sync(glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0))
{}
CRTInputBufferBuilder::~CRTInputBufferBuilder()
{
glDeleteSync(_wraparound_sync);
}
void CRTInputBufferBuilder::allocate_write_area(size_t required_length)
{
_last_allocation_amount = required_length;
if(_next_write_x_position + required_length + 2 > InputBufferBuilderWidth)
{
move_to_new_line();
}
_write_x_position = _next_write_x_position + 1;
_write_y_position = _next_write_y_position;
_write_target_pointer = (_write_y_position * InputBufferBuilderWidth) + _write_x_position;
_next_write_x_position += required_length + 2;
}
void CRTInputBufferBuilder::reduce_previous_allocation_to(size_t actual_length, uint8_t *buffer)
{
// book end the allocation with duplicates of the first and last pixel, to protect
// against rounding errors when this run is drawn
memcpy( &buffer[(_write_target_pointer - 1) * bytes_per_pixel],
&buffer[_write_target_pointer * bytes_per_pixel],
bytes_per_pixel);
memcpy( &buffer[(_write_target_pointer + actual_length) * bytes_per_pixel],
&buffer[(_write_target_pointer + actual_length - 1) * bytes_per_pixel],
bytes_per_pixel);
// return any allocated length that wasn't actually used to the available pool
_next_write_x_position -= (_last_allocation_amount - actual_length);
}

View File

@ -0,0 +1,57 @@
//
// CRTInputBufferBuilder.hpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTInputBufferBuilder_hpp
#define CRTInputBufferBuilder_hpp
#include <stdint.h>
#include <stdarg.h>
#include <stddef.h>
#include "CRTConstants.hpp"
#include "OpenGL.hpp"
namespace Outputs {
namespace CRT {
struct CRTInputBufferBuilder {
CRTInputBufferBuilder(size_t bytes_per_pixel);
~CRTInputBufferBuilder();
void allocate_write_area(size_t required_length);
void reduce_previous_allocation_to(size_t actual_length, uint8_t *buffer);
// a pointer to the section of content buffer currently being
// returned and to where the next section will begin
uint16_t _next_write_x_position, _next_write_y_position;
uint16_t _write_x_position, _write_y_position;
size_t _write_target_pointer;
size_t _last_allocation_amount;
size_t bytes_per_pixel;
// Storage for the amount of buffer uploaded so far; initialised correctly by the buffer
// builder but otherwise entrusted to the CRT to update.
unsigned int last_uploaded_line;
GLsync _wraparound_sync;
inline void move_to_new_line()
{
_next_write_x_position = 0;
_next_write_y_position = (_next_write_y_position+1)%InputBufferBuilderHeight;
}
inline uint8_t *get_write_target(uint8_t *buffer)
{
return &buffer[_write_target_pointer * bytes_per_pixel];
}
};
}
}
#endif /* CRTInputBufferBuilder_hpp */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
//
// CRTOpenGL.hpp
// Clock Signal
//
// Created by Thomas Harte on 13/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTOpenGL_h
#define CRTOpenGL_h
#include "../CRTTypes.hpp"
#include "CRTConstants.hpp"
#include "OpenGL.hpp"
#include "TextureTarget.hpp"
#include "Shader.hpp"
#include "CRTInputBufferBuilder.hpp"
#include "CRTRunBuilder.hpp"
#include <mutex>
namespace Outputs {
namespace CRT {
class OpenGLOutputBuilder {
private:
// colour information
ColourSpace _colour_space;
unsigned int _colour_cycle_numerator;
unsigned int _colour_cycle_denominator;
OutputDevice _output_device;
// timing information to allow reasoning about input information
unsigned int _cycles_per_line;
unsigned int _height_of_display;
unsigned int _horizontal_scan_period;
unsigned int _vertical_scan_period;
unsigned int _vertical_period_divider;
// The user-supplied visible area
Rect _visible_area;
// Other things the caller may have provided.
char *_composite_shader;
char *_rgb_shader;
// Methods used by the OpenGL code
void prepare_rgb_output_shader();
void prepare_composite_output_shader();
std::unique_ptr<OpenGL::Shader> prepare_output_shader(char *vertex_shader, char *fragment_shader, GLint source_texture_unit);
void prepare_composite_input_shader();
std::unique_ptr<OpenGL::Shader> prepare_intermediate_shader(const char *input_position, const char *header, char *fragment_shader, GLenum texture_unit, bool extends);
void prepare_output_vertex_array();
void prepare_source_vertex_array();
void push_size_uniforms(unsigned int output_width, unsigned int output_height);
// the run and input data buffers
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
CRTRunBuilder **_run_builders;
int _run_write_pointer;
std::shared_ptr<std::mutex> _output_mutex;
// transient buffers indicating composite data not yet decoded
uint16_t _composite_src_output_y, _cleared_composite_output_y;
char *get_output_vertex_shader(const char *header);
char *get_rgb_output_vertex_shader();
char *get_composite_output_vertex_shader();
char *get_output_fragment_shader(const char *sampling_function, const char *header, const char *fragColour_function);
char *get_rgb_output_fragment_shader();
char *get_composite_output_fragment_shader();
char *get_input_vertex_shader(const char *input_position, const char *header);
char *get_input_fragment_shader();
char *get_y_filter_fragment_shader();
char *get_chrominance_filter_fragment_shader();
std::unique_ptr<OpenGL::Shader> rgb_shader_program;
std::unique_ptr<OpenGL::Shader> composite_input_shader_program, composite_y_filter_shader_program, composite_chrominance_filter_shader_program, composite_output_shader_program;
GLuint output_array_buffer, output_vertex_array;
GLuint source_array_buffer, source_vertex_array;
GLint windowSizeUniform, timestampBaseUniform;
GLint boundsOriginUniform, boundsSizeUniform;
GLuint textureName, shadowMaskTextureName;
GLuint defaultFramebuffer;
std::unique_ptr<OpenGL::TextureTarget> compositeTexture; // receives raw composite levels
std::unique_ptr<OpenGL::TextureTarget> filteredYTexture; // receives filtered Y in the R channel plus unfiltered I/U and Q/V in G and B
std::unique_ptr<OpenGL::TextureTarget> filteredTexture; // receives filtered YIQ or YUV
void perform_output_stage(unsigned int output_width, unsigned int output_height, OpenGL::Shader *const shader);
void set_timing_uniforms();
void set_colour_space_uniforms();
public:
OpenGLOutputBuilder(unsigned int buffer_depth);
~OpenGLOutputBuilder();
inline void set_colour_format(ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator)
{
_colour_space = colour_space;
_colour_cycle_numerator = colour_cycle_numerator;
_colour_cycle_denominator = colour_cycle_denominator;
set_colour_space_uniforms();
}
inline void set_visible_area(Rect visible_area)
{
_visible_area = visible_area;
}
inline uint8_t *get_next_source_run()
{
_output_mutex->lock();
return &_source_buffer_data[_source_buffer_data_pointer % SourceVertexBufferDataSize];
}
inline void complete_source_run()
{
_source_buffer_data_pointer += 2 * SourceVertexSize;
_output_mutex->unlock();
}
inline uint8_t *get_next_output_run()
{
_output_mutex->lock();
return &_output_buffer_data[_output_buffer_data_pointer];
}
inline void complete_output_run(GLsizei vertices_written)
{
_run_builders[_run_write_pointer]->amount_of_data += (size_t)(vertices_written * OutputVertexSize);
_output_buffer_data_pointer = (_output_buffer_data_pointer + vertices_written * OutputVertexSize) % OutputVertexBufferDataSize;
_output_mutex->unlock();
}
inline OutputDevice get_output_device()
{
return _output_device;
}
inline uint32_t get_current_field_time()
{
return _run_builders[_run_write_pointer]->duration;
}
inline void add_to_field_time(uint32_t amount)
{
_run_builders[_run_write_pointer]->duration += amount;
}
inline uint16_t get_composite_output_y()
{
return _composite_src_output_y % IntermediateBufferHeight;
}
inline void increment_composite_output_y()
{
_composite_src_output_y++;
}
inline void increment_field()
{
_output_mutex->lock();
_run_write_pointer = (_run_write_pointer + 1)%NumberOfFields;
_run_builders[_run_write_pointer]->start = (size_t)_output_buffer_data_pointer;
_run_builders[_run_write_pointer]->reset();
_output_mutex->unlock();
}
inline int get_current_field()
{
return _run_write_pointer;
}
inline uint8_t *allocate_write_area(size_t required_length)
{
_output_mutex->lock();
_buffer_builder->allocate_write_area(required_length);
uint8_t *output = _input_texture_data ? _buffer_builder->get_write_target(_input_texture_data) : nullptr;
_output_mutex->unlock();
return output;
}
inline void reduce_previous_allocation_to(size_t actual_length)
{
_buffer_builder->reduce_previous_allocation_to(actual_length, _input_texture_data);
}
inline uint16_t get_last_write_x_posiiton()
{
return _buffer_builder->_write_x_position;
}
inline uint16_t get_last_write_y_posiiton()
{
return _buffer_builder->_write_y_position;
}
void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty);
void set_openGL_context_will_change(bool should_delete_resources);
void set_composite_sampling_function(const char *shader);
void set_rgb_sampling_function(const char *shader);
void set_output_device(OutputDevice output_device);
void set_timing(unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider);
uint8_t *_input_texture_data;
GLuint _input_texture_array;
GLsync _input_texture_sync;
GLsizeiptr _input_texture_array_size;
uint8_t *_output_buffer_data;
GLsizei _output_buffer_data_pointer;
uint8_t *_source_buffer_data;
GLsizei _source_buffer_data_pointer;
GLsizei _drawn_source_buffer_data_pointer;
};
}
}
#endif /* CRTOpenGL_h */

View File

@ -0,0 +1,12 @@
//
// CRTFrameBuilder.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "CRT.hpp"
#include "CRTOpenGL.hpp"
using namespace Outputs::CRT;

View File

@ -0,0 +1,41 @@
//
// CRTRunBuilder.h
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTRunBuilder_h
#define CRTRunBuilder_h
#import <vector>
namespace Outputs {
namespace CRT {
struct CRTRunBuilder {
CRTRunBuilder() : start(0) { reset(); }
// Resets the run builder.
inline void reset()
{
duration = 0;
amount_of_uploaded_data = 0;
amount_of_data = 0;
}
// Container for total length in cycles of all contained runs.
uint32_t duration;
size_t start;
// Storage for the length of run data uploaded so far; reset to zero by reset but otherwise
// entrusted to the CRT to update.
size_t amount_of_uploaded_data;
size_t amount_of_data;
};
}
}
#endif /* CRTRunBuilder_h */

View File

@ -0,0 +1,229 @@
//
// Flywheel.hpp
// Clock Signal
//
// Created by Thomas Harte on 11/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Flywheel_hpp
#define Flywheel_hpp
#include <stdlib.h>
namespace Outputs {
namespace CRT {
/*!
Provides timing for a two-phase signal consisting of a retrace phase followed by a scan phase,
announcing the start and end of retrace and providing the abiliy to read the current
scanning position.
The @c Flywheel will attempt to converge with timing implied by synchronisation pulses.
*/
struct Flywheel
{
/*!
Constructs an instance of @c Flywheel.
@param standard_period The expected amount of time between one synchronisation and the next.
@param retrace_time The amount of time it takes to complete a retrace.
*/
Flywheel(unsigned int standard_period, unsigned int retrace_time) :
_standard_period(standard_period),
_retrace_time(retrace_time),
_sync_error_window(standard_period >> 7),
_counter(0),
_expected_next_sync(standard_period),
_counter_before_retrace(standard_period - retrace_time),
_number_of_surprises(0) {}
enum SyncEvent {
/// Indicates that no synchronisation events will occur in the queried window.
None,
/// Indicates that the next synchronisation event will be a transition into retrce.
StartRetrace,
/// Indicates that the next synchronisation event will be a transition out of retrace.
EndRetrace
};
/*!
Asks the flywheel for the first synchronisation event that will occur in a given time period,
indicating whether a synchronisation request occurred at the start of the query window.
@param sync_is_requested @c true indicates that the flywheel should act as though having
received a synchronisation request now; @c false indicates no such event was detected.
@param cycles_to_run_for The number of cycles to look ahead.
@param cycles_advanced After this method has completed, contains the amount of time until
the returned synchronisation event.
@returns The next synchronisation event.
*/
inline SyncEvent get_next_event_in_period(bool sync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
// do we recognise this hsync, thereby adjusting future time expectations?
if(sync_is_requested)
{
if(_counter < _sync_error_window || _counter > _expected_next_sync - _sync_error_window)
{
unsigned int time_now = (_counter < _sync_error_window) ? _expected_next_sync + _counter : _counter;
_expected_next_sync = (3*_expected_next_sync + time_now) >> 2;
}
else
{
_number_of_surprises++;
if(_counter < _retrace_time + (_expected_next_sync >> 1))
{
_expected_next_sync = (3*_expected_next_sync + _standard_period + _sync_error_window) >> 2;
}
else
{
_expected_next_sync = (3*_expected_next_sync + _standard_period - _sync_error_window) >> 2;
}
}
}
SyncEvent proposed_event = SyncEvent::None;
unsigned int proposed_sync_time = cycles_to_run_for;
// will we end an ongoing retrace?
if(_counter < _retrace_time && _counter + proposed_sync_time >= _retrace_time)
{
proposed_sync_time = _retrace_time - _counter;
proposed_event = SyncEvent::EndRetrace;
}
// will we start a retrace?
if(_counter + proposed_sync_time >= _expected_next_sync)
{
proposed_sync_time = _expected_next_sync - _counter;
proposed_event = SyncEvent::StartRetrace;
}
*cycles_advanced = proposed_sync_time;
return proposed_event;
}
/*!
Advances a nominated amount of time, applying a previously returned synchronisation event
at the end of that period.
@param cycles_advanced The amount of time to run for.
@param event The synchronisation event to apply after that period.
*/
inline void apply_event(unsigned int cycles_advanced, SyncEvent event)
{
_counter += cycles_advanced;
switch(event)
{
default: return;
case StartRetrace:
_counter_before_retrace = _counter - _retrace_time;
_counter = 0;
return;
}
}
/*!
Returns the current output position; while in retrace this will go down towards 0, while in scan
it will go upward.
@returns The current output position.
*/
inline unsigned int get_current_output_position()
{
if(_counter < _retrace_time)
{
unsigned int retrace_distance = (_counter * _standard_period) / _retrace_time;
if(retrace_distance > _counter_before_retrace) return 0;
return _counter_before_retrace - retrace_distance;
}
return _counter - _retrace_time;
}
/*!
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
*/
inline unsigned int get_current_time()
{
return _counter;
}
/*!
@returns whether the output is currently retracing.
*/
inline bool is_in_retrace()
{
return _counter < _retrace_time;
}
/*!
@returns the expected length of the scan period (excluding retrace).
*/
inline unsigned int get_scan_period()
{
return _standard_period - _retrace_time;
}
/*!
@returns the expected length of a complete scan and retrace cycle.
*/
inline unsigned int get_standard_period()
{
return _standard_period;
}
/*!
@returns the number of synchronisation events that have seemed surprising since the last time this method was called;
a low number indicates good synchronisation.
*/
inline unsigned int get_and_reset_number_of_surprises()
{
unsigned int result = _number_of_surprises;
_number_of_surprises = 0;
return result;
}
/*!
@returns `true` if a sync is expected soon or the time at which it was expected was recent.
*/
inline bool is_near_expected_sync()
{
return abs((int)_counter - (int)_expected_next_sync) < (int)_standard_period / 50;
}
private:
unsigned int _standard_period; // the normal length of time between syncs
const unsigned int _retrace_time; // a constant indicating the amount of time it takes to perform a retrace
const unsigned int _sync_error_window; // a constant indicating the window either side of the next expected sync in which we'll accept other syncs
unsigned int _counter; // time since the _start_ of the last sync
unsigned int _counter_before_retrace; // the value of _counter immediately before retrace began
unsigned int _expected_next_sync; // our current expection of when the next sync will be encountered (which implies velocity)
unsigned int _number_of_surprises; // a count of the surprising syncs
/*
Implementation notes:
Retrace takes a fixed amount of time and runs during [0, _retrace_time).
For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point
retrace begins and the internal counter is reset.
All synchronisation events that occur within (-_sync_error_window, _sync_error_window) of the
expected synchronisation time will cause a proportional adjustment in the expected time for the next
synchronisation. Other synchronisation events are clamped as though they occurred in that range.
*/
};
}
}
#endif /* Flywheel_hpp */

View File

@ -0,0 +1,21 @@
//
// OpenGL.h
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef OpenGL_h
#define OpenGL_h
// TODO: figure out correct include paths for other platforms.
#ifdef __APPLE__
#if TARGET_OS_IPHONE
#else
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#endif
#endif
#endif /* OpenGL_h */

View File

@ -0,0 +1,99 @@
//
// Shader.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "Shader.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace OpenGL;
GLuint Shader::compile_shader(const char *source, GLenum type)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
#if defined(DEBUG)
GLint isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
if(isCompiled == GL_FALSE)
{
GLint logLength;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
GLchar *log = (GLchar *)malloc((size_t)logLength);
glGetShaderInfoLog(shader, logLength, &logLength, log);
printf("Compile log:\n%s\n", log);
free(log);
}
throw (type == GL_VERTEX_SHADER) ? VertexShaderCompilationError : FragmentShaderCompilationError;
}
#endif
return shader;
}
Shader::Shader(const char *vertex_shader, const char *fragment_shader, const AttributeBinding *attribute_bindings)
{
_shader_program = glCreateProgram();
GLuint vertex = compile_shader(vertex_shader, GL_VERTEX_SHADER);
GLuint fragment = compile_shader(fragment_shader, GL_FRAGMENT_SHADER);
glAttachShader(_shader_program, vertex);
glAttachShader(_shader_program, fragment);
if(attribute_bindings)
{
while(attribute_bindings->name)
{
glBindAttribLocation(_shader_program, attribute_bindings->index, attribute_bindings->name);
attribute_bindings++;
}
}
glLinkProgram(_shader_program);
#if defined(DEBUG)
GLint didLink = 0;
glGetProgramiv(_shader_program, GL_LINK_STATUS, &didLink);
if(didLink == GL_FALSE)
{
GLint logLength;
glGetProgramiv(_shader_program, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
GLchar *log = (GLchar *)malloc((size_t)logLength);
glGetProgramInfoLog(_shader_program, logLength, &logLength, log);
printf("Link log:\n%s\n", log);
free(log);
}
throw ProgramLinkageError;
}
#endif
}
Shader::~Shader()
{
glDeleteProgram(_shader_program);
}
void Shader::bind()
{
glUseProgram(_shader_program);
}
GLint Shader::get_attrib_location(const GLchar *name)
{
return glGetAttribLocation(_shader_program, name);
}
GLint Shader::get_uniform_location(const GLchar *name)
{
return glGetUniformLocation(_shader_program, name);
}

View File

@ -0,0 +1,70 @@
//
// Shader.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Shader_hpp
#define Shader_hpp
#include "OpenGL.hpp"
namespace OpenGL {
/*!
A @c Shader compiles and holds a shader object, based on a single
vertex program and a single fragment program. Attribute bindings
may be supplied if desired.
*/
class Shader {
public:
enum {
VertexShaderCompilationError,
FragmentShaderCompilationError,
ProgramLinkageError
};
struct AttributeBinding {
const GLchar *name;
GLuint index;
};
/*!
Attempts to compile a shader, throwing @c VertexShaderCompilationError, @c FragmentShaderCompilationError or @c ProgramLinkageError upon failure.
@param vertex_shader The vertex shader source code.
@param fragment_shader The fragment shader source code.
@param attribute_bindings Either @c nullptr or an array terminated by an entry with a @c nullptr-name of attribute bindings.
*/
Shader(const char *vertex_shader, const char *fragment_shader, const AttributeBinding *attribute_bindings);
~Shader();
/*!
Performs an @c glUseProgram to make this the active shader.
*/
void bind();
/*!
Performs a @c glGetAttribLocation call.
@param name The name of the attribute to locate.
@returns The location of the requested attribute.
*/
GLint get_attrib_location(const GLchar *name);
/*!
Performs a @c glGetUniformLocation call.
@param name The name of the uniform to locate.
@returns The location of the requested uniform.
*/
GLint get_uniform_location(const GLchar *name);
private:
GLuint compile_shader(const char *source, GLenum type);
GLuint _shader_program;
};
}
#endif /* Shader_hpp */

View File

@ -0,0 +1,45 @@
//
// TextureTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "TextureTarget.hpp"
using namespace OpenGL;
TextureTarget::TextureTarget(GLsizei width, GLsizei height) : _width(width), _height(height)
{
glGenFramebuffers(1, &_framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
glGenTextures(1, &_texture);
glBindTexture(GL_TEXTURE_2D, _texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
throw ErrorFramebufferIncomplete;
}
TextureTarget::~TextureTarget()
{
glDeleteFramebuffers(1, &_framebuffer);
glDeleteTextures(1, &_texture);
}
void TextureTarget::bind_framebuffer()
{
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
glViewport(0, 0, _width, _height);
}
void TextureTarget::bind_texture()
{
glBindTexture(GL_TEXTURE_2D, _texture);
}

View File

@ -0,0 +1,54 @@
//
// TextureTarget.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef TextureTarget_hpp
#define TextureTarget_hpp
#include "OpenGL.hpp"
namespace OpenGL {
/*!
A @c TextureTarget is a framebuffer that can be bound as a texture. So this class
handles render-to-texture framebuffer objects.
*/
class TextureTarget {
public:
/*!
Creates a new texture target.
Throws ErrorFramebufferIncomplete if creation fails.
@param width The width of target to create.
@param height The height of target to create.
*/
TextureTarget(GLsizei width, GLsizei height);
~TextureTarget();
/*!
Binds this target as a framebuffer and sets the @c glViewport accordingly.
*/
void bind_framebuffer();
/*!
Binds this target as a texture.
*/
void bind_texture();
enum {
ErrorFramebufferIncomplete
};
private:
GLuint _framebuffer, _texture;
GLsizei _width, _height;
};
}
#endif /* TextureTarget_hpp */

View File

@ -1,51 +0,0 @@
//
// CRTFrame.h
// Clock Signal
//
// Created by Thomas Harte on 24/07/2015.
// Copyright © 2015 Thomas Harte. All rights reserved.
//
#ifndef CRTFrame_h
#define CRTFrame_h
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint8_t *data;
unsigned int depth;
} CRTBuffer;
typedef struct {
uint16_t width, height;
} CRTSize;
typedef struct {
CRTSize size, dirty_size;
unsigned int number_of_buffers;
CRTBuffer *buffers;
unsigned int number_of_runs;
uint8_t *runs;
} CRTFrame;
typedef uint16_t kCRTPositionType;
typedef uint16_t kCRTTexCoordType;
typedef uint8_t kCRTLateralType;
typedef uint8_t kCRTPhaseType;
static const size_t kCRTVertexOffsetOfPosition = 0;
static const size_t kCRTVertexOffsetOfTexCoord = 4;
static const size_t kCRTVertexOffsetOfLateral = 8;
static const size_t kCRTVertexOffsetOfPhase = 9;
static const int kCRTSizeOfVertex = 10;
#ifdef __cplusplus
}
#endif
#endif /* CRTFrame_h */

12
Outputs/Speaker.cpp Normal file
View File

@ -0,0 +1,12 @@
//
// Speaker.cpp
// Clock Signal
//
// Created by Thomas Harte on 12/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "Speaker.hpp"
using namespace Outputs;

158
Outputs/Speaker.hpp Normal file
View File

@ -0,0 +1,158 @@
//
// Speaker.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Speaker_hpp
#define Speaker_hpp
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include "../SignalProcessing/Stepper.hpp"
#include "../SignalProcessing/FIRFilter.hpp"
namespace Outputs {
class Speaker {
public:
class Delegate {
public:
virtual void speaker_did_complete_samples(Speaker *speaker, const int16_t *buffer, int buffer_size) = 0;
};
void set_output_rate(int cycles_per_second, int buffer_size)
{
_output_cycles_per_second = cycles_per_second;
if(_buffer_size != buffer_size)
{
_buffer_in_progress = std::unique_ptr<int16_t>(new int16_t[buffer_size]);
_buffer_size = buffer_size;
}
set_needs_updated_filter_coefficients();
}
void set_output_quality(int number_of_taps)
{
_requested_number_of_taps = number_of_taps;
set_needs_updated_filter_coefficients();
}
void set_delegate(Delegate *delegate)
{
_delegate = delegate;
}
void set_input_rate(int cycles_per_second)
{
_input_cycles_per_second = cycles_per_second;
set_needs_updated_filter_coefficients();
}
Speaker() : _buffer_in_progress_pointer(0), _requested_number_of_taps(0) {}
protected:
std::unique_ptr<int16_t> _buffer_in_progress;
int _buffer_size;
int _buffer_in_progress_pointer;
int _number_of_taps, _requested_number_of_taps;
bool _coefficients_are_dirty;
Delegate *_delegate;
int _input_cycles_per_second, _output_cycles_per_second;
void set_needs_updated_filter_coefficients()
{
_coefficients_are_dirty = true;
}
};
template <class T> class Filter: public Speaker {
public:
void run_for_cycles(unsigned int input_cycles)
{
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)
{
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)
{
_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;
}
}
}
}
private:
std::unique_ptr<SignalProcessing::Stepper> _stepper;
std::unique_ptr<SignalProcessing::FIRFilter> _filter;
std::unique_ptr<int16_t> _input_buffer;
int _input_buffer_depth;
void update_filter_coefficients()
{
// make a guess at a good number of taps if this hasn't been provided explicitly
if(_requested_number_of_taps)
{
_number_of_taps = _requested_number_of_taps;
}
else
{
_number_of_taps = (_input_cycles_per_second + _output_cycles_per_second) / _output_cycles_per_second;
_number_of_taps *= 2;
_number_of_taps |= 1;
}
_coefficients_are_dirty = false;
_buffer_in_progress_pointer = 0;
_stepper = std::unique_ptr<SignalProcessing::Stepper>(new SignalProcessing::Stepper((uint64_t)_input_cycles_per_second, (uint64_t)_output_cycles_per_second));
_filter = std::unique_ptr<SignalProcessing::FIRFilter>(new SignalProcessing::FIRFilter((unsigned int)_number_of_taps, (unsigned int)_input_cycles_per_second, 0.0, (float)_output_cycles_per_second / 2.0f, SignalProcessing::FIRFilter::DefaultAttenuation));
_input_buffer = std::unique_ptr<int16_t>(new int16_t[_number_of_taps]);
_input_buffer_depth = 0;
}
};
}
#endif /* Speaker_hpp */

View File

@ -14,6 +14,9 @@
namespace CPU6502 {
/*
The list of registers that can be accessed via @c set_value_of_register and @c set_value_of_register.
*/
enum Register {
LastOperationAddress,
ProgramCounter,
@ -25,7 +28,9 @@ enum Register {
S
};
/*
Flags as defined on the 6502; can be used to decode the result of @c get_flags or to form a value for @c set_flags.
*/
enum Flag {
Sign = 0x80,
Overflow = 0x40,
@ -37,14 +42,35 @@ enum Flag {
Carry = 0x01
};
/*!
Subclasses will be given the task of performing bus operations, allowing them to provide whatever interface they like
between a 6502 and the rest of the system. @c BusOperation lists the types of bus operation that may be requested.
@c None is reserved for internal use. It will never be requested from a subclass. It is safe always to use the
isReadOperation macro to make a binary choice between reading and writing.
*/
enum BusOperation {
Read, ReadOpcode, Write, Ready, None
};
/*!
Evaluates to `true` if the operation is a read; `false` if it is a write.
*/
#define isReadOperation(v) (v == CPU6502::BusOperation::Read || v == CPU6502::BusOperation::ReadOpcode)
/*!
An opcode that is guaranteed to cause the CPU to jam.
*/
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.
*/
template <class T> class Processor {
public:
@ -55,10 +81,16 @@ template <class T> class Processor {
private:
/*
This emulation funcitons by decomposing instructions into micro programs, consisting of the micro operations
as per the enum below. Each micro op takes at most one cycle. By convention, those called CycleX take a cycle
to perform whereas those called OperationX occur for free (so, in effect, their cost is loaded onto the next cycle).
*/
enum MicroOp {
CycleFetchOperation, CycleFetchOperand, OperationDecodeOperation, CycleIncPCPushPCH,
CyclePushPCH, CyclePushPCL, CyclePushA, CyclePushOperand,
CycleSetIReadBRKLow, CycleReadBRKHigh, CycleReadFromS, CycleReadFromPC,
CycleSetIReadBRKLow, CycleReadBRKHigh,
CycleReadFromS, CycleReadFromPC,
CyclePullOperand, CyclePullPCL, CyclePullPCH, CyclePullA,
CycleReadAndIncrementPC, CycleIncrementPCAndReadStack, CycleIncrementPCReadPCHLoadPCL, CycleReadPCHLoadPCL,
CycleReadAddressHLoadAddressL, CycleReadPCLFromAddress, CycleReadPCHFromAddress, CycleLoadAddressAbsolute,
@ -84,6 +116,7 @@ template <class T> class Processor {
OperationTAY, OperationTAX, OperationTSX, OperationARR,
OperationSBX, OperationLXA, OperationANE, OperationANC,
OperationLAS, CycleAddSignedOperandToPC, OperationSetFlagsFromOperand, OperationSetOperandFromFlagsWithBRKSet,
OperationSetOperandFromFlags,
OperationSetFlagsFromA, CycleReadRSTLow, CycleReadRSTHigh, CycleScheduleJam
};
@ -96,33 +129,69 @@ template <class T> class Processor {
} bytes;
};
/*
Storage for the 6502 registers; F is stored as individual flags.
*/
RegisterPair _pc, _lastOperationPC;
uint8_t _a, _x, _y, _s;
uint8_t _carryFlag, _negativeResult, _zeroResult, _decimalFlag, _overflowFlag, _interruptFlag;
/*
Temporary state for the micro programs.
*/
uint8_t _operation, _operand;
RegisterPair _address, _nextAddress;
/*
Up to four programs can be scheduled; each will be carried out in turn. This
storage maintains pointers to the scheduled list of programs.
Programs should be terminated by an OperationMoveToNextProgram, causing this
queue to take that step.
*/
const MicroOp *_scheduledPrograms[4];
unsigned int _scheduleProgramsWritePointer, _scheduleProgramsReadPointer, _scheduleProgramProgramCounter;
/*
Temporary storage allowing a common dispatch point for calling perform_bus_operation;
possibly deferring is no longer of value.
*/
BusOperation _nextBusOperation;
uint16_t _busAddress;
uint8_t *_busValue;
uint64_t _externalBus;
/*!
Schedules a new program, adding it to the end of the queue. Programs should be
terminated with a OperationMoveToNextProgram. No attempt to copy the program
is made; a non-owning reference is kept.
void schedule_program(const MicroOp *program)
@param program The program to schedule.
*/
inline void schedule_program(const MicroOp *program)
{
_scheduledPrograms[_scheduleProgramsWritePointer] = program;
_scheduleProgramsWritePointer = (_scheduleProgramsWritePointer+1)&3;
}
/*!
Gets the flags register.
@see set_flags
@returns The current value of the flags register.
*/
uint8_t get_flags()
{
return _carryFlag | _overflowFlag | _interruptFlag | (_negativeResult & 0x80) | (_zeroResult ? 0 : Flag::Zero) | Flag::Always | _decimalFlag;
}
/*!
Sets the flags register.
@see set_flags
@param flags The new value of the flags register.
*/
void set_flags(uint8_t flags)
{
_carryFlag = flags & Flag::Carry;
@ -133,7 +202,12 @@ template <class T> class Processor {
_decimalFlag = flags & Flag::Decimal;
}
void decode_operation(uint8_t operation)
/*!
Schedules the program corresponding to the specified operation.
@param operation The operation code for which to schedule a program.
*/
inline void decode_operation(uint8_t operation)
{
#define Program(...) {__VA_ARGS__, OperationMoveToNextProgram}
@ -378,21 +452,16 @@ template <class T> class Processor {
bool _ready_line_is_enabled;
bool _reset_line_is_enabled;
bool _irq_line_is_enabled, _irq_request_history[2];
bool _nmi_line_is_enabled;
bool _ready_is_active;
public:
Processor() :
_scheduleProgramsReadPointer(0),
_scheduleProgramsWritePointer(0),
_is_jammed(false),
_jam_handler(nullptr),
_cycles_left_to_run(0),
_ready_line_is_enabled(false),
_ready_is_active(false),
_scheduledPrograms{nullptr, nullptr, nullptr, nullptr}
{}
/*!
Gets the program representing an RST response.
const MicroOp *get_reset_program() {
@returns The program representing an RST response.
*/
inline const MicroOp *get_reset_program() {
static const MicroOp reset[] = {
CycleFetchOperand,
CycleFetchOperand,
@ -406,6 +475,60 @@ template <class T> class Processor {
return reset;
}
/*!
Gets the program representing an IRQ response.
@returns The program representing an IRQ response.
*/
inline const MicroOp *get_irq_program() {
static const MicroOp reset[] = {
CyclePushPCH,
CyclePushPCL,
OperationSetOperandFromFlags,
CyclePushOperand,
CycleSetIReadBRKLow,
CycleReadBRKHigh,
OperationMoveToNextProgram
};
return reset;
}
protected:
Processor() :
_scheduleProgramsReadPointer(0),
_scheduleProgramsWritePointer(0),
_is_jammed(false),
_jam_handler(nullptr),
_cycles_left_to_run(0),
_ready_line_is_enabled(false),
_ready_is_active(false),
_scheduledPrograms{nullptr, nullptr, nullptr, nullptr},
_interruptFlag(Flag::Interrupt),
_s(0),
_nextBusOperation(BusOperation::None)
{
// only the interrupt flag is defined upon reset but get_flags isn't going to
// mask the other flags so we need to do that, at least
_carryFlag &= Flag::Carry;
_decimalFlag &= Flag::Decimal;
_overflowFlag &= Flag::Overflow;
// TODO: is this accurate? It feels more likely that a CPU would need to wait
// on an explicit reset command, since the relative startup times of different
// components from power on would be a bit unpredictable.
schedule_program(get_reset_program());
}
public:
/*!
Runs the 6502 for a supplied number of cycles.
@discussion Subclasses must implement @c perform_bus_operation(BusOperation operation, uint16_t address, uint8_t *value) .
The 6502 will call that method for all bus accesses. The 6502 is guaranteed to perform one bus operation call per cycle.
@param number_of_cycles The number of cycles to run the 6502 for.
*/
void run_for_cycles(int number_of_cycles)
{
static const MicroOp doBranch[] = {
@ -422,39 +545,56 @@ template <class T> class Processor {
OperationMoveToNextProgram
};
// These plus program below act to give the compiler permission to update these values
// without touching the class storage (i.e. it explicitly says they need be completely up
// to date in this stack frame only); which saves some complicated addressing
unsigned int scheduleProgramsReadPointer = _scheduleProgramsReadPointer;
unsigned int scheduleProgramProgramCounter = _scheduleProgramProgramCounter;
RegisterPair nextAddress = _nextAddress;
BusOperation nextBusOperation = _nextBusOperation;
uint16_t busAddress = _busAddress;
uint8_t *busValue = _busValue;
#define checkSchedule(op) \
if(!_scheduledPrograms[_scheduleProgramsReadPointer]) {\
_scheduleProgramsReadPointer = _scheduleProgramsWritePointer = _scheduleProgramProgramCounter = 0;\
if(_reset_line_is_enabled)\
if(!_scheduledPrograms[scheduleProgramsReadPointer]) {\
scheduleProgramsReadPointer = _scheduleProgramsWritePointer = scheduleProgramProgramCounter = 0;\
if(_reset_line_is_enabled) {\
schedule_program(get_reset_program());\
else\
schedule_program(fetch_decode_execute);\
} else {\
if(_irq_request_history[0])\
schedule_program(get_irq_program());\
else\
schedule_program(fetch_decode_execute);\
}\
op;\
}
checkSchedule();
_cycles_left_to_run += number_of_cycles;
number_of_cycles += _cycles_left_to_run;
const MicroOp *program = _scheduledPrograms[scheduleProgramsReadPointer];
while(_cycles_left_to_run > 0) {
while(number_of_cycles > 0) {
while (_ready_is_active && _cycles_left_to_run > 0) {
_cycles_left_to_run -= static_cast<T *>(this)->perform_bus_operation(BusOperation::Ready, _busAddress, _busValue);
while (_ready_is_active && number_of_cycles > 0) {
number_of_cycles -= static_cast<T *>(this)->perform_bus_operation(BusOperation::Ready, busAddress, busValue);
}
while (!_ready_is_active && _cycles_left_to_run > 0) {
while (!_ready_is_active && number_of_cycles > 0) {
if (_nextBusOperation != BusOperation::None) {
_cycles_left_to_run -= static_cast<T *>(this)->perform_bus_operation(_nextBusOperation, _busAddress, _busValue);
_nextBusOperation = BusOperation::None;
if(nextBusOperation != BusOperation::None) {
_irq_request_history[0] = _irq_request_history[1];
_irq_request_history[1] = _irq_line_is_enabled && !_interruptFlag;
number_of_cycles -= static_cast<T *>(this)->perform_bus_operation(nextBusOperation, busAddress, busValue);
nextBusOperation = BusOperation::None;
}
const MicroOp cycle = _scheduledPrograms[_scheduleProgramsReadPointer][_scheduleProgramProgramCounter];
_scheduleProgramProgramCounter++;
const MicroOp cycle = program[scheduleProgramProgramCounter];
scheduleProgramProgramCounter++;
#define read_op(val, addr) _nextBusOperation = BusOperation::ReadOpcode; _busAddress = addr; _busValue = &val
#define read_mem(val, addr) _nextBusOperation = BusOperation::Read; _busAddress = addr; _busValue = &val
#define throwaway_read(addr) _nextBusOperation = BusOperation::Read; _busAddress = addr; _busValue = &throwaway_target
#define write_mem(val, addr) _nextBusOperation = BusOperation::Write; _busAddress = addr; _busValue = &val
#define read_op(val, addr) nextBusOperation = BusOperation::ReadOpcode; busAddress = addr; busValue = &val
#define read_mem(val, addr) nextBusOperation = BusOperation::Read; busAddress = addr; busValue = &val
#define throwaway_read(addr) nextBusOperation = BusOperation::Read; busAddress = addr; busValue = &throwaway_target
#define write_mem(val, addr) nextBusOperation = BusOperation::Write; busAddress = addr; busValue = &val
switch(cycle) {
@ -486,10 +626,11 @@ template <class T> class Processor {
break;
case OperationMoveToNextProgram:
_scheduledPrograms[_scheduleProgramsReadPointer] = NULL;
_scheduleProgramsReadPointer = (_scheduleProgramsReadPointer+1)&3;
_scheduleProgramProgramCounter = 0;
_scheduledPrograms[scheduleProgramsReadPointer] = NULL;
scheduleProgramsReadPointer = (scheduleProgramsReadPointer+1)&3;
scheduleProgramProgramCounter = 0;
checkSchedule();
program = _scheduledPrograms[scheduleProgramsReadPointer];
break;
#define push(v) \
@ -520,6 +661,7 @@ template <class T> class Processor {
case CyclePullOperand: _s++; read_mem(_operand, _s | 0x100); break;
case OperationSetFlagsFromOperand: set_flags(_operand); break;
case OperationSetOperandFromFlagsWithBRKSet: _operand = get_flags() | Flag::Break; break;
case OperationSetOperandFromFlags: _operand = get_flags(); break;
case OperationSetFlagsFromA: _zeroResult = _negativeResult = _a; break;
case CycleIncrementPCAndReadStack: _pc.full++; throwaway_read(_s | 0x100); break;
@ -539,7 +681,7 @@ template <class T> class Processor {
static const MicroOp jam[] = JAM;
schedule_program(jam);
if (_jam_handler) {
if(_jam_handler) {
_jam_handler->processor_did_jam(this, _pc.full - 1);
checkSchedule(_is_jammed = false);
}
@ -754,31 +896,31 @@ template <class T> class Processor {
#pragma mark - Addressing Mode Work
case CycleAddXToAddressLow:
_nextAddress.full = _address.full + _x;
_address.bytes.low = _nextAddress.bytes.low;
if (_address.bytes.high != _nextAddress.bytes.high) {
nextAddress.full = _address.full + _x;
_address.bytes.low = nextAddress.bytes.low;
if(_address.bytes.high != nextAddress.bytes.high) {
throwaway_read(_address.full);
}
break;
case CycleAddXToAddressLowRead:
_nextAddress.full = _address.full + _x;
_address.bytes.low = _nextAddress.bytes.low;
nextAddress.full = _address.full + _x;
_address.bytes.low = nextAddress.bytes.low;
throwaway_read(_address.full);
break;
case CycleAddYToAddressLow:
_nextAddress.full = _address.full + _y;
_address.bytes.low = _nextAddress.bytes.low;
if (_address.bytes.high != _nextAddress.bytes.high) {
nextAddress.full = _address.full + _y;
_address.bytes.low = nextAddress.bytes.low;
if(_address.bytes.high != nextAddress.bytes.high) {
throwaway_read(_address.full);
}
break;
case CycleAddYToAddressLowRead:
_nextAddress.full = _address.full + _y;
_address.bytes.low = _nextAddress.bytes.low;
nextAddress.full = _address.full + _y;
_address.bytes.low = nextAddress.bytes.low;
throwaway_read(_address.full);
break;
case OperationCorrectAddressHigh:
_address.full = _nextAddress.full;
_address.full = nextAddress.full;
break;
case CycleIncrementPCFetchAddressLowFromOperand:
_pc.full++;
@ -849,11 +991,11 @@ template <class T> class Processor {
case OperationBEQ: BRA(!_zeroResult); break;
case CycleAddSignedOperandToPC:
_nextAddress.full = (uint16_t)(_pc.full + (int8_t)_operand);
_pc.bytes.low = _nextAddress.bytes.low;
if(_nextAddress.bytes.high != _pc.bytes.high) {
nextAddress.full = (uint16_t)(_pc.full + (int8_t)_operand);
_pc.bytes.low = nextAddress.bytes.low;
if(nextAddress.bytes.high != _pc.bytes.high) {
uint16_t halfUpdatedPc = _pc.full;
_pc.full = _nextAddress.full;
_pc.full = nextAddress.full;
throwaway_read(halfUpdatedPc);
}
break;
@ -877,11 +1019,10 @@ template <class T> class Processor {
_zeroResult = _negativeResult = _a;
_overflowFlag = (_a^(_a << 1))&Flag::Overflow;
if ((unshiftedA&0xf) + (unshiftedA&0x1) > 5) _a = ((_a + 6)&0xf) | (_a & 0xf0);
if((unshiftedA&0xf) + (unshiftedA&0x1) > 5) _a = ((_a + 6)&0xf) | (_a & 0xf0);
_carryFlag = ((unshiftedA&0xf0) + (unshiftedA&0x10) > 0x50) ? 1 : 0;
if (_carryFlag) _a += 0x60;
if(_carryFlag) _a += 0x60;
} else {
_a &= _operand;
_a = (uint8_t)((_a >> 1) | (_carryFlag << 7));
@ -900,13 +1041,29 @@ template <class T> class Processor {
break;
}
if (isReadOperation(_nextBusOperation) && _ready_line_is_enabled) {
if(isReadOperation(nextBusOperation) && _ready_line_is_enabled) {
_ready_is_active = true;
}
}
_cycles_left_to_run = number_of_cycles;
_scheduleProgramsReadPointer = scheduleProgramsReadPointer;
_scheduleProgramProgramCounter = scheduleProgramProgramCounter;
_nextAddress = nextAddress;
_nextBusOperation = nextBusOperation;
_busAddress = busAddress;
_busValue = busValue;
}
}
/*!
Gets the value of a register.
@see set_value_of_register
@param r The register to set.
@returns The value of the register. 8-bit registers will be returned as unsigned.
*/
uint16_t get_value_of_register(Register r)
{
switch (r) {
@ -922,32 +1079,32 @@ template <class T> class Processor {
}
}
/*!
Sets the value of a register.
@see get_value_of_register
@param r The register to set.
@param value The value to set. If the register is only 8 bit, the value will be truncated.
*/
void set_value_of_register(Register r, uint16_t value)
{
switch (r) {
case Register::ProgramCounter: _pc.full = value; break;
case Register::StackPointer: _s = value; break;
case Register::Flags: set_flags(value); break;
case Register::A: _a = value; break;
case Register::X: _x = value; break;
case Register::Y: _y = value; break;
case Register::S: _s = value; break;
case Register::ProgramCounter: _pc.full = value; break;
case Register::StackPointer: _s = (uint8_t)value; break;
case Register::Flags: set_flags((uint8_t)value); break;
case Register::A: _a = (uint8_t)value; break;
case Register::X: _x = (uint8_t)value; break;
case Register::Y: _y = (uint8_t)value; break;
case Register::S: _s = (uint8_t)value; break;
default: break;
}
}
void setup6502()
{
// only the interrupt flag is defined upon reset but get_flags isn't going to
// mask the other flags so we need to do that, at least
_interruptFlag = Flag::Interrupt;
_carryFlag &= Flag::Carry;
_decimalFlag &= Flag::Decimal;
_overflowFlag &= Flag::Overflow;
_s = 0;
_nextBusOperation = BusOperation::None;
}
/*!
Interrupts current execution flow to perform an RTS and, if the 6502 is currently jammed,
to unjam it.
*/
void return_from_subroutine()
{
_s++;
@ -960,28 +1117,70 @@ template <class T> class Processor {
}
}
void set_ready_line(bool active)
/*!
Sets the current level of the RDY line.
@param active @c true if the line is logically active; @c false otherwise.
*/
inline void set_ready_line(bool active)
{
if(active)
if(active) {
_ready_line_is_enabled = true;
else
{
} else {
_ready_line_is_enabled = false;
_ready_is_active = false;
}
}
void set_reset_line(bool active)
/*!
Sets the current level of the RST line.
@param active @c true if the line is logically active; @c false otherwise.
*/
inline void set_reset_line(bool active)
{
_reset_line_is_enabled = active;
}
bool is_jammed()
/*!
Sets the current level of the IRQ line.
@param active @c true if the line is logically active; @c false otherwise.
*/
inline void set_irq_line(bool active)
{
_irq_line_is_enabled = active;
}
/*!
Sets the current level of the NMI line.
@param active `true` if the line is logically active; `false` otherwise.
*/
inline void set_nmi_line(bool active)
{
// TODO: NMI is edge triggered, not level, and in any case _nmi_line_is_enabled
// is not honoured elsewhere. So NMI is yet to be implemented.
_nmi_line_is_enabled = active;
}
/*!
Queries whether the 6502 is now 'jammed'; i.e. has entered an invalid state
such that it will not of itself perform any more meaningful processing.
@returns @c true if the 6502 is jammed; @c false otherwise.
*/
inline bool is_jammed()
{
return _is_jammed;
}
void set_jam_handler(JamHandler *handler)
/*!
Installs a jam handler. Jam handlers are notified if a running 6502 jams.
@param handler The class instance that will be this 6502's jam handler from now on.
*/
inline void set_jam_handler(JamHandler *handler)
{
_jam_handler = handler;
}

View File

@ -12,10 +12,7 @@
using namespace CPU6502;
AllRAMProcessor::AllRAMProcessor() : _timestamp(0)
{
setup6502();
}
AllRAMProcessor::AllRAMProcessor() : _timestamp(0) {}
int AllRAMProcessor::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value)
{

View File

@ -0,0 +1,157 @@
//
// LinearFilter.c
// Clock Signal
//
// Created by Thomas Harte on 01/10/2011.
// Copyright 2011 Thomas Harte. All rights reserved.
//
#include "FIRFilter.hpp"
#include <math.h>
using namespace SignalProcessing;
/*
A Kaiser-Bessel filter is a real time window filter. It looks at the last n samples
of an incoming data source and computes a filtered value, which is the value you'd
get after applying the specified filter, at the centre of the sampling window.
Hence, if you request a 37 tap filter then filtering introduces a latency of 18
samples. Suppose you're receiving input at 44,100Hz and using 4097 taps, then you'll
introduce a latency of 2048 samples, which is about 46ms.
There's a correlation between the number of taps and the quality of the filtering.
More samples = better filtering, at the cost of greater latency. Internally, applying
the filter involves calculating a weighted sum of previous values, so increasing the
number of taps is quite cheap in processing terms.
Original source for this filter:
"DIGITAL SIGNAL PROCESSING, II", IEEE Press, pages 123126.
*/
// our little fixed point scheme
#define kCSKaiserBesselFilterFixedMultiplier 32767.0f
#define kCSKaiserBesselFilterFixedShift 15
/* ino evaluates the 0th order Bessel function at a */
float FIRFilter::ino(float a)
{
float d = 0.0f;
float ds = 1.0f;
float s = 1.0f;
do
{
d += 2.0f;
ds *= (a * a) / (d * d);
s += ds;
}
while(ds > s*1e-6f);
return s;
}
//static void csfilter_setIdealisedFilterResponse(short *filterCoefficients, float *A, float attenuation, unsigned int numberOfTaps)
void FIRFilter::coefficients_for_idealised_filter_response(short *filterCoefficients, float *A, float attenuation, unsigned int numberOfTaps)
{
/* calculate alpha, which is the Kaiser-Bessel window shape factor */
float a; // to take the place of alpha in the normal derivation
if(attenuation < 21.0f)
a = 0.0f;
else
{
if(attenuation > 50.0f)
a = 0.1102f * (attenuation - 8.7f);
else
a = 0.5842f * powf(attenuation - 21.0f, 0.4f) + 0.7886f * (attenuation - 21.0f);
}
float *filterCoefficientsFloat = new float[numberOfTaps];
/* work out the right hand side of the filter coefficients */
unsigned int Np = (numberOfTaps - 1) / 2;
float I0 = ino(a);
float NpSquared = (float)(Np * Np);
for(unsigned int i = 0; i <= Np; i++)
{
filterCoefficientsFloat[Np + i] =
A[i] *
ino(a * sqrtf(1.0f - ((float)(i * i) / NpSquared) )) /
I0;
}
/* coefficients are symmetrical, so copy from right hand side to left side */
for(unsigned int i = 0; i < Np; i++)
{
filterCoefficientsFloat[i] = filterCoefficientsFloat[numberOfTaps - 1 - i];
}
/* scale back up so that we retain 100% of input volume */
float coefficientTotal = 0.0f;
for(unsigned int i = 0; i < numberOfTaps; i++)
{
coefficientTotal += filterCoefficientsFloat[i];
}
/* we'll also need integer versions, potentially */
float coefficientMultiplier = 1.0f / coefficientTotal;
for(unsigned int i = 0; i < numberOfTaps; i++)
{
filterCoefficients[i] = (short)(filterCoefficientsFloat[i] * kCSKaiserBesselFilterFixedMultiplier * coefficientMultiplier);
}
delete[] filterCoefficientsFloat;
}
void FIRFilter::get_coefficients(float *coefficients)
{
for(unsigned int i = 0; i < number_of_taps_; i++)
{
coefficients[i] = (float)filter_coefficients_[i] / kCSKaiserBesselFilterFixedMultiplier;
}
}
FIRFilter::FIRFilter(unsigned int number_of_taps, float input_sample_rate, float low_frequency, float high_frequency, float attenuation)
{
// we must be asked to filter based on an odd number of
// taps, and at least three
if(number_of_taps < 3) number_of_taps = 3;
if(attenuation < 21.0f) attenuation = 21.0f;
// ensure we have an odd number of taps
number_of_taps |= 1;
// store instance variables
number_of_taps_ = number_of_taps;
filter_coefficients_ = new short[number_of_taps_];
/* calculate idealised filter response */
unsigned int Np = (number_of_taps - 1) / 2;
float twoOverSampleRate = 2.0f / input_sample_rate;
float *A = new float[Np+1];
A[0] = 2.0f * (high_frequency - low_frequency) / input_sample_rate;
for(unsigned int i = 1; i <= Np; i++)
{
float iPi = (float)i * (float)M_PI;
A[i] =
(
sinf(twoOverSampleRate * iPi * high_frequency) -
sinf(twoOverSampleRate * iPi * low_frequency)
) / iPi;
}
FIRFilter::coefficients_for_idealised_filter_response(filter_coefficients_, A, attenuation, number_of_taps_);
/* clean up */
delete[] A;
}
FIRFilter::~FIRFilter()
{
delete[] filter_coefficients_;
}

View File

@ -0,0 +1,93 @@
//
// LinearFilter.h
// Clock Signal
//
// Created by Thomas Harte on 01/10/2011.
// Copyright 2011 Thomas Harte. All rights reserved.
//
#ifndef FIRFilter_hpp
#define FIRFilter_hpp
/*
The FIR filter takes a 1d PCM signal with
a given sample rate and filters it according
to a specified filter (band pass only at
present, more to come if required). The number
of taps (ie, samples considered simultaneously
to make an output sample) is configurable;
smaller numbers permit a filter that operates
more quickly and with less lag but less
effectively.
FIR filters are window functions; expected use is
to point sample an input that has been subject to
a filter.
*/
#ifdef __APPLE__
#include <Accelerate/Accelerate.h>
#endif
namespace SignalProcessing {
class FIRFilter {
public:
/*!
Creates an instance of @c FIRFilter.
@param number_of_taps The size of window for input data.
@param input_sample_rate The sampling rate of the input signal.
@param low_frequency The lowest frequency of signal to retain in the output.
@param high_frequency The highest frequency of signal to retain in the output.
@param attenuation The attenuation of the discarded frequencies.
*/
FIRFilter(unsigned int number_of_taps, float input_sample_rate, float low_frequency, float high_frequency, float attenuation);
~FIRFilter();
/*! A suggested default attenuation value. */
constexpr static float DefaultAttenuation = 60.0f;
/*!
Applies the filter to one batch of input samples, returning the net result.
@param src The source buffer to apply the filter to.
@returns The result of applying the filter.
*/
inline short apply(const short *src)
{
#ifdef __APPLE__
short result;
vDSP_dotpr_s1_15(filter_coefficients_, 1, src, 1, &result, number_of_taps_);
return result;
#else
int outputValue = 0;
for(unsigned int c = 0; c < number_of_taps_; c++)
{
outputValue += filter_coefficients_[c] * src[c];
}
return (short)(outputValue >> kCSKaiserBesselFilterFixedShift);
#endif
}
inline unsigned int get_number_of_taps()
{
return number_of_taps_;
}
void get_coefficients(float *coefficients);
private:
short *filter_coefficients_;
unsigned int number_of_taps_;
static void coefficients_for_idealised_filter_response(short *filterCoefficients, float *A, float attenuation, unsigned int numberOfTaps);
static float ino(float a);
};
}
#endif

View File

@ -0,0 +1,60 @@
//
// Stepper.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Stepper_hpp
#define Stepper_hpp
#include <stdint.h>
namespace SignalProcessing {
class Stepper
{
public:
Stepper() : Stepper(1,1) {}
Stepper(uint64_t output_rate, uint64_t input_rate) :
accumulated_error_(0),
input_rate_(input_rate),
output_rate_(output_rate),
whole_step_(output_rate / input_rate),
adjustment_up_((int64_t)(output_rate % input_rate) << 1),
adjustment_down_((int64_t)input_rate << 1) {}
inline uint64_t step()
{
uint64_t update = whole_step_;
accumulated_error_ += adjustment_up_;
if(accumulated_error_ > 0)
{
update++;
accumulated_error_ -= adjustment_down_;
}
return update;
}
inline uint64_t get_output_rate()
{
return output_rate_;
}
inline uint64_t get_input_rate()
{
return input_rate_;
}
private:
uint64_t whole_step_;
int64_t adjustment_up_, adjustment_down_;
int64_t accumulated_error_;
uint64_t input_rate_, output_rate_;
};
}
#endif /* Stepper_hpp */

View File

@ -0,0 +1,303 @@
//
// TapeUEF.cpp
// Clock Signal
//
// Created by Thomas Harte on 18/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "TapeUEF.hpp"
#include <string.h>
#include <math.h>
static float gzgetfloat(gzFile file)
{
uint8_t bytes[4];
bytes[0] = (uint8_t)gzgetc(file);
bytes[1] = (uint8_t)gzgetc(file);
bytes[2] = (uint8_t)gzgetc(file);
bytes[3] = (uint8_t)gzgetc(file);
/* assume a four byte array named Float exists, where Float[0]
was the first byte read from the UEF, Float[1] the second, etc */
/* decode mantissa */
int mantissa;
mantissa = bytes[0] | (bytes[1] << 8) | ((bytes[2]&0x7f)|0x80) << 16;
float result = (float)mantissa;
result = (float)ldexp(result, -23);
/* decode exponent */
int exponent;
exponent = ((bytes[2]&0x80) >> 7) | (bytes[3]&0x7f) << 1;
exponent -= 127;
result = (float)ldexp(result, exponent);
/* flip sign if necessary */
if(bytes[3]&0x80)
result = -result;
return result;
}
Storage::UEF::UEF(const char *file_name) :
_chunk_id(0), _chunk_length(0), _chunk_position(0),
_time_base(1200)
{
_file = gzopen(file_name, "rb");
char identifier[10];
int bytes_read = gzread(_file, identifier, 10);
if(bytes_read < 10 || strcmp(identifier, "UEF File!"))
{
throw ErrorNotUEF;
}
int minor, major;
minor = gzgetc(_file);
major = gzgetc(_file);
if(major > 0 || minor > 10 || major < 0 || minor < 0)
{
throw ErrorNotUEF;
}
_start_of_next_chunk = gztell(_file);
find_next_tape_chunk();
}
Storage::UEF::~UEF()
{
gzclose(_file);
}
void Storage::UEF::reset()
{
gzseek(_file, 12, SEEK_SET);
}
Storage::Tape::Pulse Storage::UEF::get_next_pulse()
{
Pulse next_pulse;
if(!_bit_position && chunk_is_finished())
{
find_next_tape_chunk();
}
switch(_chunk_id)
{
case 0x0100: case 0x0102:
// In the ordinary ("1200 baud") data encoding format,
// a zero bit is encoded as one complete cycle at the base frequency.
// A one bit is two complete cycles at twice the base frequency.
if(!_bit_position)
{
_current_bit = get_next_bit();
}
next_pulse.type = (_bit_position&1) ? Pulse::High : Pulse::Low;
next_pulse.length.length = _current_bit ? 1 : 2;
next_pulse.length.clock_rate = _time_base * 4;
_bit_position = (_bit_position+1)&(_current_bit ? 3 : 1);
break;
case 0x0110:
next_pulse.type = (_bit_position&1) ? Pulse::High : Pulse::Low;
next_pulse.length.length = 1;
next_pulse.length.clock_rate = _time_base * 4;
_bit_position ^= 1;
if(!_bit_position) _chunk_position++;
break;
case 0x0114:
if(!_bit_position)
{
_current_bit = get_next_bit();
if(_first_is_pulse && !_chunk_position)
{
_bit_position++;
}
}
next_pulse.type = (_bit_position&1) ? Pulse::High : Pulse::Low;
next_pulse.length.length = _current_bit ? 1 : 2;
next_pulse.length.clock_rate = _time_base * 4;
_bit_position ^= 1;
if((_chunk_id == 0x0114) && (_chunk_position == _chunk_duration.length-1) && _last_is_pulse)
{
_chunk_position++;
}
break;
case 0x0112:
case 0x0116:
next_pulse.type = Pulse::Zero;
next_pulse.length = _chunk_duration;
_chunk_position++;
break;
}
return next_pulse;
}
void Storage::UEF::find_next_tape_chunk()
{
int reset_count = 0;
_chunk_position = 0;
_bit_position = 0;
while(1)
{
gzseek(_file, _start_of_next_chunk, SEEK_SET);
// read chunk ID
_chunk_id = (uint16_t)gzgetc(_file);
_chunk_id |= (uint16_t)(gzgetc(_file) << 8);
_chunk_length = (uint32_t)(gzgetc(_file) << 0);
_chunk_length |= (uint32_t)(gzgetc(_file) << 8);
_chunk_length |= (uint32_t)(gzgetc(_file) << 16);
_chunk_length |= (uint32_t)(gzgetc(_file) << 24);
_start_of_next_chunk = gztell(_file) + _chunk_length;
if(gzeof(_file))
{
reset_count++;
if(reset_count == 2) break;
reset();
continue;
}
switch(_chunk_id)
{
case 0x0100: // implicit bit pattern
_implicit_data_chunk.position = 0;
return;
case 0x0102: // explicit bit patterns
_explicit_data_chunk.position = 0;
return;
case 0x0112: // integer gap
_chunk_duration.length = (uint16_t)gzgetc(_file);
_chunk_duration.length |= (uint16_t)(gzgetc(_file) << 8);
_chunk_duration.clock_rate = _time_base;
return;
case 0x0116: // floating point gap
{
float length = gzgetfloat(_file);
_chunk_duration.length = (unsigned int)(length * 4000000);
_chunk_duration.clock_rate = 4000000;
}
return;
case 0x0110: // carrier tone
_chunk_duration.length = (uint16_t)gzgetc(_file);
_chunk_duration.length |= (uint16_t)(gzgetc(_file) << 8);
gzseek(_file, _chunk_length - 2, SEEK_CUR);
return;
// case 0x0111: // carrier tone with dummy byte
// TODO: read lengths
// return;
case 0x0114: // security cycles
{
// read number of cycles
_chunk_duration.length = (uint32_t)gzgetc(_file);
_chunk_duration.length |= (uint32_t)gzgetc(_file) << 8;
_chunk_duration.length |= (uint32_t)gzgetc(_file) << 16;
// Ps and Ws
_first_is_pulse = gzgetc(_file) == 'P';
_last_is_pulse = gzgetc(_file) == 'P';
}
break;
case 0x113: // change of base rate
{
// TODO: something smarter than just converting this to an int
float new_time_base = gzgetfloat(_file);
_time_base = (unsigned int)roundf(new_time_base);
}
break;
default:
gzseek(_file, _chunk_length, SEEK_CUR);
break;
}
}
}
bool Storage::UEF::chunk_is_finished()
{
switch(_chunk_id)
{
case 0x0100: return (_implicit_data_chunk.position / 10) == _chunk_length;
case 0x0102: return (_explicit_data_chunk.position / 8) == _chunk_length;
case 0x0114:
case 0x0110: return _chunk_position == _chunk_duration.length;
case 0x0112:
case 0x0116: return _chunk_position ? true : false;
default: return true;
}
}
bool Storage::UEF::get_next_bit()
{
switch(_chunk_id)
{
case 0x0100:
{
uint32_t bit_position = _implicit_data_chunk.position%10;
_implicit_data_chunk.position++;
if(!bit_position) _implicit_data_chunk.current_byte = (uint8_t)gzgetc(_file);
if(bit_position == 0) return false;
if(bit_position == 9) return true;
bool result = (_implicit_data_chunk.current_byte&1) ? true : false;
_implicit_data_chunk.current_byte >>= 1;
return result;
}
break;
case 0x0102:
{
uint32_t bit_position = _explicit_data_chunk.position%8;
_explicit_data_chunk.position++;
if(!bit_position) _explicit_data_chunk.current_byte = (uint8_t)gzgetc(_file);
bool result = (_explicit_data_chunk.current_byte&1) ? true : false;
_explicit_data_chunk.current_byte >>= 1;
return result;
}
break;
// TODO: 0x0104, 0x0111
case 0x0114:
{
uint32_t bit_position = _chunk_position%8;
_chunk_position++;
if(!bit_position && _chunk_position < _chunk_duration.length)
{
_current_byte = (uint8_t)gzgetc(_file);
}
bool result = (_current_byte&1) ? true : false;
_current_byte >>= 1;
return result;
}
break;
case 0x0110:
_chunk_position++;
return true;
default: return true;
}
}

View File

@ -0,0 +1,68 @@
//
// TapeUEF.hpp
// Clock Signal
//
// Created by Thomas Harte on 18/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef TapeUEF_hpp
#define TapeUEF_hpp
#include "../Tape.hpp"
#include <zlib.h>
#include <stdint.h>
namespace Storage {
class UEF : public Tape {
public:
UEF(const char *file_name);
~UEF();
Pulse get_next_pulse();
void reset();
enum {
ErrorNotUEF
};
private:
gzFile _file;
unsigned int _time_base;
z_off_t _start_of_next_chunk;
uint16_t _chunk_id;
uint32_t _chunk_length;
union {
struct {
uint8_t current_byte;
uint32_t position;
} _implicit_data_chunk;
struct {
uint8_t current_byte;
uint32_t position;
} _explicit_data_chunk;
};
uint8_t _current_byte;
uint32_t _chunk_position;
bool _current_bit;
uint32_t _bit_position;
Time _chunk_duration;
bool _first_is_pulse;
bool _last_is_pulse;
void find_next_tape_chunk();
bool get_next_bit();
bool chunk_is_finished();
};
}
#endif /* TapeUEF_hpp */

16
Storage/Tape/Tape.cpp Normal file
View File

@ -0,0 +1,16 @@
//
// Tape.cpp
// Clock Signal
//
// Created by Thomas Harte on 18/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "Tape.hpp"
using namespace Storage;
void Tape::seek(Tape::Time seek_time)
{
// TODO: as best we can
}

39
Storage/Tape/Tape.hpp Normal file
View File

@ -0,0 +1,39 @@
//
// Tape.hpp
// Clock Signal
//
// Created by Thomas Harte on 18/01/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Tape_hpp
#define Tape_hpp
#include <stdio.h>
namespace Storage {
class Tape {
public:
struct Time {
unsigned int length, clock_rate;
};
struct Pulse {
enum {
High, Low, Zero
} type;
Time length;
};
virtual Pulse get_next_pulse() = 0;
virtual void reset() = 0;
virtual void seek(Time seek_time);
};
}
#endif /* Tape_hpp */