mirror of
https://github.com/TomHarte/CLK.git
synced 2025-01-27 06:35:04 +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:
commit
6d769b3639
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
@ -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) {
|
@ -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;
|
1038
Machines/Electron/Electron.cpp
Normal file
1038
Machines/Electron/Electron.cpp
Normal file
File diff suppressed because it is too large
Load Diff
229
Machines/Electron/Electron.hpp
Normal file
229
Machines/Electron/Electron.hpp
Normal 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 */
|
@ -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 */ = {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
83
OSBindings/Mac/Clock Signal/Base.lproj/ElectronDocument.xib
Normal file
83
OSBindings/Mac/Clock Signal/Base.lproj/ElectronDocument.xib
Normal 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>
|
@ -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>
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
140
OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift
Normal file
140
OSBindings/Mac/Clock Signal/Documents/ElectronDocument.swift
Normal 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))
|
||||
}
|
||||
}
|
72
OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift
Normal file
72
OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift
Normal 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) {}
|
||||
}
|
@ -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>
|
||||
|
78
OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h
Normal file
78
OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h
Normal 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
|
178
OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m
Normal file
178
OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m
Normal 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
|
15
OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h
Normal file
15
OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.h
Normal 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
|
193
OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m
Normal file
193
OSBindings/Mac/Clock Signal/Wrappers/AudioQueue.m
Normal 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
|
@ -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
|
94
OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm
Normal file
94
OSBindings/Mac/Clock Signal/Wrappers/CSAtari2600.mm
Normal 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
|
27
OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h
Normal file
27
OSBindings/Mac/Clock Signal/Wrappers/CSElectron.h
Normal 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
|
180
OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm
Normal file
180
OSBindings/Mac/Clock Signal/Wrappers/CSElectron.mm
Normal 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
|
22
OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h
Normal file
22
OSBindings/Mac/Clock Signal/Wrappers/CSMachine+Subclassing.h
Normal 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
|
21
OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h
Normal file
21
OSBindings/Mac/Clock Signal/Wrappers/CSMachine.h
Normal 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
|
71
OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm
Normal file
71
OSBindings/Mac/Clock Signal/Wrappers/CSMachine.mm
Normal 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
|
151
OSBindings/Mac/Clock Signal/Wrappers/KeyCodes.h
Normal file
151
OSBindings/Mac/Clock Signal/Wrappers/KeyCodes.h
Normal 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 */
|
@ -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);
|
||||
}
|
||||
|
463
Outputs/CRT.cpp
463
Outputs/CRT.cpp
@ -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];
|
||||
}
|
129
Outputs/CRT.hpp
129
Outputs/CRT.hpp
@ -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
407
Outputs/CRT/CRT.cpp
Normal 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
276
Outputs/CRT/CRT.hpp
Normal 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
47
Outputs/CRT/CRTTypes.hpp
Normal 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 */
|
59
Outputs/CRT/Internals/CRTConstants.hpp
Normal file
59
Outputs/CRT/Internals/CRTConstants.hpp
Normal 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 */
|
57
Outputs/CRT/Internals/CRTInputBufferBuilder.cpp
Normal file
57
Outputs/CRT/Internals/CRTInputBufferBuilder.cpp
Normal 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);
|
||||
}
|
57
Outputs/CRT/Internals/CRTInputBufferBuilder.hpp
Normal file
57
Outputs/CRT/Internals/CRTInputBufferBuilder.hpp
Normal 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 */
|
1028
Outputs/CRT/Internals/CRTOpenGL.cpp
Normal file
1028
Outputs/CRT/Internals/CRTOpenGL.cpp
Normal file
File diff suppressed because it is too large
Load Diff
231
Outputs/CRT/Internals/CRTOpenGL.hpp
Normal file
231
Outputs/CRT/Internals/CRTOpenGL.hpp
Normal 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 */
|
12
Outputs/CRT/Internals/CRTRunBuilder.cpp
Normal file
12
Outputs/CRT/Internals/CRTRunBuilder.cpp
Normal 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;
|
41
Outputs/CRT/Internals/CRTRunBuilder.hpp
Normal file
41
Outputs/CRT/Internals/CRTRunBuilder.hpp
Normal 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 */
|
229
Outputs/CRT/Internals/Flywheel.hpp
Normal file
229
Outputs/CRT/Internals/Flywheel.hpp
Normal 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 */
|
21
Outputs/CRT/Internals/OpenGL.hpp
Normal file
21
Outputs/CRT/Internals/OpenGL.hpp
Normal 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 */
|
99
Outputs/CRT/Internals/Shader.cpp
Normal file
99
Outputs/CRT/Internals/Shader.cpp
Normal 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);
|
||||
}
|
70
Outputs/CRT/Internals/Shader.hpp
Normal file
70
Outputs/CRT/Internals/Shader.hpp
Normal 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 */
|
45
Outputs/CRT/Internals/TextureTarget.cpp
Normal file
45
Outputs/CRT/Internals/TextureTarget.cpp
Normal 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);
|
||||
}
|
54
Outputs/CRT/Internals/TextureTarget.hpp
Normal file
54
Outputs/CRT/Internals/TextureTarget.hpp
Normal 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 */
|
@ -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
12
Outputs/Speaker.cpp
Normal 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
158
Outputs/Speaker.hpp
Normal 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 */
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
157
SignalProcessing/FIRFilter.cpp
Normal file
157
SignalProcessing/FIRFilter.cpp
Normal 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 123–126.
|
||||
*/
|
||||
|
||||
|
||||
// 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_;
|
||||
}
|
93
SignalProcessing/FIRFilter.hpp
Normal file
93
SignalProcessing/FIRFilter.hpp
Normal 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
|
60
SignalProcessing/Stepper.hpp
Normal file
60
SignalProcessing/Stepper.hpp
Normal 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 */
|
303
Storage/Tape/Formats/TapeUEF.cpp
Normal file
303
Storage/Tape/Formats/TapeUEF.cpp
Normal 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;
|
||||
}
|
||||
}
|
68
Storage/Tape/Formats/TapeUEF.hpp
Normal file
68
Storage/Tape/Formats/TapeUEF.hpp
Normal 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
16
Storage/Tape/Tape.cpp
Normal 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
39
Storage/Tape/Tape.hpp
Normal 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 */
|
Loading…
x
Reference in New Issue
Block a user