mirror of
https://github.com/TomHarte/CLK.git
synced 2025-01-03 15:29:45 +00:00
Merge branch 'master' into FinalOverride
This commit is contained in:
commit
7316a3aa88
@ -37,9 +37,9 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
|
|||||||
return ideal / static_cast<float>(speakers_.size());
|
return ideal / static_cast<float>(speakers_.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) {
|
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size) {
|
||||||
for(const auto &speaker: speakers_) {
|
for(const auto &speaker: speakers_) {
|
||||||
speaker->set_output_rate(cycles_per_second, buffer_size);
|
speaker->set_computed_output_rate(cycles_per_second, buffer_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,9 +38,9 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
|
|||||||
void set_new_front_machine(::Machine::DynamicMachine *machine);
|
void set_new_front_machine(::Machine::DynamicMachine *machine);
|
||||||
|
|
||||||
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
||||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) final;
|
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
|
||||||
void set_output_rate(float cycles_per_second, int buffer_size) final;
|
void set_computed_output_rate(float cycles_per_second, int buffer_size) override;
|
||||||
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) final;
|
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
||||||
|
@ -337,7 +337,7 @@ class CRTCBusHandler {
|
|||||||
|
|
||||||
/// @returns The current scan status.
|
/// @returns The current scan status.
|
||||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||||
return crt_.get_scaled_scan_status() / 64.0f;
|
return crt_.get_scaled_scan_status() / 4.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the type of display.
|
/// Sets the type of display.
|
||||||
|
@ -399,7 +399,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
|||||||
via_.flush();
|
via_.flush();
|
||||||
audio_.queue.perform();
|
audio_.queue.perform();
|
||||||
|
|
||||||
// Experimental?
|
// This avoids deferring IWM costs indefinitely, until
|
||||||
|
// they become artbitrarily large.
|
||||||
iwm_.flush();
|
iwm_.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,11 +54,31 @@ class Machine {
|
|||||||
|
|
||||||
/// Runs the machine for @c duration seconds.
|
/// Runs the machine for @c duration seconds.
|
||||||
virtual void run_for(Time::Seconds duration) {
|
virtual void run_for(Time::Seconds duration) {
|
||||||
const double cycles = (duration * clock_rate_) + clock_conversion_error_;
|
const double cycles = (duration * clock_rate_ * speed_multiplier_) + clock_conversion_error_;
|
||||||
clock_conversion_error_ = std::fmod(cycles, 1.0);
|
clock_conversion_error_ = std::fmod(cycles, 1.0);
|
||||||
run_for(Cycles(static_cast<int>(cycles)));
|
run_for(Cycles(static_cast<int>(cycles)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Sets a speed multiplier to apply to this machine; e.g. a multiplier of 1.5 will cause the
|
||||||
|
emulated machine to run 50% faster than a real machine. This speed-up is an emulation
|
||||||
|
fiction: it will apply across the system, including to the CRT.
|
||||||
|
*/
|
||||||
|
virtual void set_speed_multiplier(double multiplier) {
|
||||||
|
speed_multiplier_ = multiplier;
|
||||||
|
auto speaker = get_speaker();
|
||||||
|
if(speaker) {
|
||||||
|
speaker->set_input_rate_multiplier(float(multiplier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@returns The current speed multiplier.
|
||||||
|
*/
|
||||||
|
virtual double get_speed_multiplier() {
|
||||||
|
return speed_multiplier_;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Runs for the machine for at least @c duration seconds, and then until @c condition is true.
|
Runs for the machine for at least @c duration seconds, and then until @c condition is true.
|
||||||
|
|
||||||
@ -169,6 +189,7 @@ class Machine {
|
|||||||
private:
|
private:
|
||||||
double clock_rate_ = 1.0;
|
double clock_rate_ = 1.0;
|
||||||
double clock_conversion_error_ = 0.0;
|
double clock_conversion_error_ = 0.0;
|
||||||
|
double speed_multiplier_ = 1.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -111,5 +111,5 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||||
return crt_.get_scaled_scan_status() / 0.5f;
|
return crt_.get_scaled_scan_status() / 2.0f;
|
||||||
}
|
}
|
||||||
|
@ -158,6 +158,8 @@
|
|||||||
4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; };
|
4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; };
|
||||||
4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
|
4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
|
||||||
4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; };
|
4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; };
|
||||||
|
4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03523CEB86000B98D9E /* BD500.cpp */; };
|
||||||
|
4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03323C58B1E00B98D9E /* STX.cpp */; };
|
||||||
4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; };
|
4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; };
|
||||||
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; };
|
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; };
|
||||||
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; };
|
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; };
|
||||||
@ -4252,6 +4254,7 @@
|
|||||||
4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */,
|
4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */,
|
||||||
4B055AB41FAE860F0060FFFF /* OricTAP.cpp in Sources */,
|
4B055AB41FAE860F0060FFFF /* OricTAP.cpp in Sources */,
|
||||||
4B055AB71FAE860F0060FFFF /* TZX.cpp in Sources */,
|
4B055AB71FAE860F0060FFFF /* TZX.cpp in Sources */,
|
||||||
|
4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */,
|
||||||
4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */,
|
4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */,
|
||||||
4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */,
|
4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */,
|
||||||
4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */,
|
4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */,
|
||||||
@ -4303,6 +4306,7 @@
|
|||||||
4BEBFB4E2002C4BF000708CC /* MSXDSK.cpp in Sources */,
|
4BEBFB4E2002C4BF000708CC /* MSXDSK.cpp in Sources */,
|
||||||
4B055ADD1FAE9B460060FFFF /* i8272.cpp in Sources */,
|
4B055ADD1FAE9B460060FFFF /* i8272.cpp in Sources */,
|
||||||
4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */,
|
4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */,
|
||||||
|
4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */,
|
||||||
4B0ACC2723775819008902D0 /* AtariST.cpp in Sources */,
|
4B0ACC2723775819008902D0 /* AtariST.cpp in Sources */,
|
||||||
4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */,
|
4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */,
|
||||||
4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */,
|
4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */,
|
||||||
@ -5075,6 +5079,7 @@
|
|||||||
GCC_WARN_UNUSED_LABEL = YES;
|
GCC_WARN_UNUSED_LABEL = YES;
|
||||||
INFOPLIST_FILE = "Clock Signal/Info.plist";
|
INFOPLIST_FILE = "Clock Signal/Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
OTHER_CPLUSPLUSFLAGS = (
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
"$(OTHER_CFLAGS)",
|
"$(OTHER_CFLAGS)",
|
||||||
"-Wreorder",
|
"-Wreorder",
|
||||||
@ -5121,6 +5126,7 @@
|
|||||||
GCC_WARN_UNUSED_LABEL = YES;
|
GCC_WARN_UNUSED_LABEL = YES;
|
||||||
INFOPLIST_FILE = "Clock Signal/Info.plist";
|
INFOPLIST_FILE = "Clock Signal/Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||||
OTHER_CPLUSPLUSFLAGS = (
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
"$(OTHER_CFLAGS)",
|
"$(OTHER_CFLAGS)",
|
||||||
"-Wreorder",
|
"-Wreorder",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
@implementation CSOpenGLView {
|
@implementation CSOpenGLView {
|
||||||
CVDisplayLinkRef _displayLink;
|
CVDisplayLinkRef _displayLink;
|
||||||
CGSize _backingSize;
|
CGSize _backingSize;
|
||||||
|
NSScreen *_currentScreen;
|
||||||
|
|
||||||
NSTrackingArea *_mouseTrackingArea;
|
NSTrackingArea *_mouseTrackingArea;
|
||||||
NSTimer *_mouseHideTimer;
|
NSTimer *_mouseHideTimer;
|
||||||
@ -26,12 +27,38 @@
|
|||||||
- (void)prepareOpenGL {
|
- (void)prepareOpenGL {
|
||||||
[super prepareOpenGL];
|
[super prepareOpenGL];
|
||||||
|
|
||||||
// Synchronize buffer swaps with vertical refresh rate
|
// Note the initial screen.
|
||||||
|
_currentScreen = self.window.screen;
|
||||||
|
|
||||||
|
// Synchronize buffer swaps with vertical refresh rate.
|
||||||
|
// TODO: discard this, once scheduling is sufficiently intelligent?
|
||||||
GLint swapInt = 1;
|
GLint swapInt = 1;
|
||||||
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
|
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
|
||||||
|
|
||||||
|
// set the clear colour
|
||||||
|
[self.openGLContext makeCurrentContext];
|
||||||
|
glClearColor(0.0, 0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Setup the [initial] display link.
|
||||||
|
[self setupDisplayLink];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setupDisplayLink {
|
||||||
|
// Kill the existing link if there is one, then wait until its final shout is definitely done.
|
||||||
|
if(_displayLink) {
|
||||||
|
const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
|
||||||
|
CVDisplayLinkStop(_displayLink);
|
||||||
|
|
||||||
|
// This is a workaround; I could find no way to ensure that a callback from the display
|
||||||
|
// link is not currently ongoing.
|
||||||
|
usleep((useconds_t)ceil(duration * 1000000.0));
|
||||||
|
|
||||||
|
CVDisplayLinkRelease(_displayLink);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a display link capable of being used with all active displays
|
// Create a display link capable of being used with all active displays
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
|
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
|
||||||
|
CVDisplayLinkCreateWithCGDisplay(screenNumber.unsignedIntValue, &_displayLink);
|
||||||
|
|
||||||
// Set the renderer output callback function
|
// Set the renderer output callback function
|
||||||
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
|
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
|
||||||
@ -41,21 +68,44 @@
|
|||||||
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
|
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
|
||||||
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
|
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
|
||||||
|
|
||||||
// set the clear colour
|
|
||||||
[self.openGLContext makeCurrentContext];
|
|
||||||
glClearColor(0.0, 0.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
// Activate the display link
|
// Activate the display link
|
||||||
CVDisplayLinkStart(_displayLink);
|
CVDisplayLinkStart(_displayLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
|
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
|
||||||
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
|
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
|
||||||
|
|
||||||
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
|
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
|
||||||
|
/*
|
||||||
|
Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink.
|
||||||
|
Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
|
||||||
|
it doesn't wait for completion of any current calls. So I've set up a usleep for one callback's duration,
|
||||||
|
so code in here gets one callback's duration to access the display link.
|
||||||
|
|
||||||
|
In practice, it should do so only upon entry, and before calling into the view. The view promises not to
|
||||||
|
access the display link itself as part of -drawAtTime:frequency:.
|
||||||
|
*/
|
||||||
|
|
||||||
return kCVReturnSuccess;
|
return kCVReturnSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
|
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
|
||||||
|
// Test now whether the screen this view is on has changed since last time it was checked.
|
||||||
|
// There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification,
|
||||||
|
// but since this method is going to be called repeatedly anyway, and the test is cheap, polling
|
||||||
|
// feels fine.
|
||||||
|
if(self.window.screen != _currentScreen) {
|
||||||
|
_currentScreen = self.window.screen;
|
||||||
|
|
||||||
|
// Issue a reshape, in case a switch to/from a Retina display has
|
||||||
|
// happened, changing the results of -convertSizeToBacking:, etc.
|
||||||
|
[self reshape];
|
||||||
|
|
||||||
|
// Also switch display links, to make sure synchronisation is with the display
|
||||||
|
// the window is actually on, and at its rate.
|
||||||
|
[self setupDisplayLink];
|
||||||
|
}
|
||||||
|
|
||||||
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
|
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +125,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
// Release the display link
|
// Stop and release the display link
|
||||||
|
CVDisplayLinkStop(_displayLink);
|
||||||
CVDisplayLinkRelease(_displayLink);
|
CVDisplayLinkRelease(_displayLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,10 +34,12 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate {
|
struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate {
|
||||||
Time::Seconds update(Concurrency::BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) final {
|
Time::Seconds update(Concurrency::BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) override {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(*machine_mutex);
|
||||||
return machine->crt_machine()->run_until(duration, flags);
|
return machine->crt_machine()->run_until(duration, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::mutex *machine_mutex;
|
||||||
Machine::DynamicMachine *machine;
|
Machine::DynamicMachine *machine;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,6 +105,7 @@ class ActivityObserver: public Activity::Observer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void set_aspect_ratio(float aspect_ratio) {
|
void set_aspect_ratio(float aspect_ratio) {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
lights_.clear();
|
lights_.clear();
|
||||||
|
|
||||||
// Generate a bunch of LEDs for connected drives.
|
// Generate a bunch of LEDs for connected drives.
|
||||||
@ -129,6 +132,7 @@ class ActivityObserver: public Activity::Observer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void draw() {
|
void draw() {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
for(const auto &lit_led: lit_leds_) {
|
for(const auto &lit_led: lit_leds_) {
|
||||||
if(blinking_leds_.find(lit_led) == blinking_leds_.end() && lights_.find(lit_led) != lights_.end())
|
if(blinking_leds_.find(lit_led) == blinking_leds_.end() && lights_.find(lit_led) != lights_.end())
|
||||||
lights_[lit_led]->draw(0.0, 0.8, 0.0);
|
lights_[lit_led]->draw(0.0, 0.8, 0.0);
|
||||||
@ -139,26 +143,31 @@ class ActivityObserver: public Activity::Observer {
|
|||||||
private:
|
private:
|
||||||
std::vector<std::string> leds_;
|
std::vector<std::string> leds_;
|
||||||
void register_led(const std::string &name) final {
|
void register_led(const std::string &name) final {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
leds_.push_back(name);
|
leds_.push_back(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> drives_;
|
std::vector<std::string> drives_;
|
||||||
void register_drive(const std::string &name) final {
|
void register_drive(const std::string &name) final {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
drives_.push_back(name);
|
drives_.push_back(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_led_status(const std::string &name, bool lit) final {
|
void set_led_status(const std::string &name, bool lit) final {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
if(lit) lit_leds_.insert(name);
|
if(lit) lit_leds_.insert(name);
|
||||||
else lit_leds_.erase(name);
|
else lit_leds_.erase(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void announce_drive_event(const std::string &name, DriveEvent event) final {
|
void announce_drive_event(const std::string &name, DriveEvent event) final {
|
||||||
|
std::lock_guard<std::mutex> lock_guard(mutex);
|
||||||
blinking_leds_.insert(name);
|
blinking_leds_.insert(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<std::string, std::unique_ptr<Outputs::Display::OpenGL::Rectangle>> lights_;
|
std::map<std::string, std::unique_ptr<Outputs::Display::OpenGL::Rectangle>> lights_;
|
||||||
std::set<std::string> lit_leds_;
|
std::set<std::string> lit_leds_;
|
||||||
std::set<std::string> blinking_leds_;
|
std::set<std::string> blinking_leds_;
|
||||||
|
std::mutex mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool KeyboardKeyForSDLScancode(SDL_Keycode scancode, Inputs::Keyboard::Key &key) {
|
bool KeyboardKeyForSDLScancode(SDL_Keycode scancode, Inputs::Keyboard::Key &key) {
|
||||||
@ -336,7 +345,7 @@ int main(int argc, char *argv[]) {
|
|||||||
ParsedArguments arguments = parse_arguments(argc, argv);
|
ParsedArguments arguments = parse_arguments(argc, argv);
|
||||||
|
|
||||||
// This may be printed either as
|
// This may be printed either as
|
||||||
const std::string usage_suffix = " [file] [OPTIONS] [--rompath={path to ROMs}]";
|
const std::string usage_suffix = " [file] [OPTIONS] [--rompath={path to ROMs}] [--speed={speed multiplier, e.g. 1.5}]";
|
||||||
|
|
||||||
// Print a help message if requested.
|
// Print a help message if requested.
|
||||||
if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) {
|
if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) {
|
||||||
@ -401,7 +410,7 @@ int main(int argc, char *argv[]) {
|
|||||||
"/usr/share/CLK/"
|
"/usr/share/CLK/"
|
||||||
};
|
};
|
||||||
if(arguments.selections.find("rompath") != arguments.selections.end()) {
|
if(arguments.selections.find("rompath") != arguments.selections.end()) {
|
||||||
std::string user_path = arguments.selections["rompath"]->list_selection()->value;
|
const std::string user_path = arguments.selections["rompath"]->list_selection()->value;
|
||||||
if(user_path.back() != '/') {
|
if(user_path.back() != '/') {
|
||||||
paths.push_back(user_path + "/");
|
paths.push_back(user_path + "/");
|
||||||
} else {
|
} else {
|
||||||
@ -442,6 +451,7 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// Create and configure a machine.
|
// Create and configure a machine.
|
||||||
::Machine::Error error;
|
::Machine::Error error;
|
||||||
|
std::mutex machine_mutex;
|
||||||
std::unique_ptr<::Machine::DynamicMachine> machine(::Machine::MachineForTargets(targets, rom_fetcher, error));
|
std::unique_ptr<::Machine::DynamicMachine> machine(::Machine::MachineForTargets(targets, rom_fetcher, error));
|
||||||
if(!machine) {
|
if(!machine) {
|
||||||
switch(error) {
|
switch(error) {
|
||||||
@ -462,7 +472,25 @@ int main(int argc, char *argv[]) {
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the speed multiplier, if one was requested.
|
||||||
|
if(arguments.selections.find("speed") != arguments.selections.end()) {
|
||||||
|
const char *speed_string = arguments.selections["speed"]->list_selection()->value.c_str();
|
||||||
|
char *end;
|
||||||
|
double speed = strtod(speed_string, &end);
|
||||||
|
|
||||||
|
if(end-speed_string != strlen(speed_string)) {
|
||||||
|
std::cerr << "Unable to parse speed: " << speed_string << std::endl;
|
||||||
|
} else if(speed <= 0.0) {
|
||||||
|
std::cerr << "Cannot run at speed " << speed_string << "; speeds must be positive." << std::endl;
|
||||||
|
} else {
|
||||||
|
machine->crt_machine()->set_speed_multiplier(speed);
|
||||||
|
// TODO: what if not a 'CRT' machine? Likely rests on resolving this project's machine naming policy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up the best-effort updater, its delegate, and the speaker delegate.
|
||||||
best_effort_updater_delegate.machine = machine.get();
|
best_effort_updater_delegate.machine = machine.get();
|
||||||
|
best_effort_updater_delegate.machine_mutex = &machine_mutex;
|
||||||
speaker_delegate.updater = &updater;
|
speaker_delegate.updater = &updater;
|
||||||
updater.set_delegate(&best_effort_updater_delegate);
|
updater.set_delegate(&best_effort_updater_delegate);
|
||||||
|
|
||||||
@ -617,7 +645,17 @@ int main(int argc, char *argv[]) {
|
|||||||
bool should_quit = false;
|
bool should_quit = false;
|
||||||
Uint32 fullscreen_mode = 0;
|
Uint32 fullscreen_mode = 0;
|
||||||
while(!should_quit) {
|
while(!should_quit) {
|
||||||
// Process all pending events.
|
// Wait for vsync, draw a new frame and post a machine update.
|
||||||
|
// NB: machine_mutex is *not* currently locked, therefore it shouldn't
|
||||||
|
// be 'most' of the time.
|
||||||
|
SDL_GL_SwapWindow(window);
|
||||||
|
scan_target.update(int(window_width), int(window_height));
|
||||||
|
scan_target.draw(int(window_width), int(window_height));
|
||||||
|
if(activity_observer) activity_observer->draw();
|
||||||
|
updater.update();
|
||||||
|
|
||||||
|
// Grab the machine lock and process all pending events.
|
||||||
|
std::lock_guard<std::mutex> lock_guard(machine_mutex);
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while(SDL_PollEvent(&event)) {
|
while(SDL_PollEvent(&event)) {
|
||||||
switch(event.type) {
|
switch(event.type) {
|
||||||
@ -643,87 +681,88 @@ int main(int argc, char *argv[]) {
|
|||||||
} break;
|
} break;
|
||||||
|
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN:
|
||||||
// Syphon off the key-press if it's control+shift+V (paste).
|
case SDL_KEYUP: {
|
||||||
if(event.key.keysym.sym == SDLK_v && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
|
const auto keyboard_machine = machine->keyboard_machine();
|
||||||
const auto keyboard_machine = machine->keyboard_machine();
|
|
||||||
if(keyboard_machine) {
|
if(event.type == SDL_KEYDOWN) {
|
||||||
keyboard_machine->type_string(SDL_GetClipboardText());
|
// Syphon off the key-press if it's control+shift+V (paste).
|
||||||
|
if(event.key.keysym.sym == SDLK_v && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
|
||||||
|
if(keyboard_machine) {
|
||||||
|
keyboard_machine->type_string(SDL_GetClipboardText());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ctrl+escape to release the mouse (if captured).
|
||||||
|
if(event.key.keysym.sym == SDLK_ESCAPE && (SDL_GetModState()&KMOD_CTRL)) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
window_titler.set_mouse_is_captured(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture ctrl+shift+d as a take-a-screenshot command.
|
||||||
|
if(event.key.keysym.sym == SDLK_d && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
|
||||||
|
// Grab the screen buffer.
|
||||||
|
Outputs::Display::OpenGL::Screenshot screenshot(4, 3);
|
||||||
|
|
||||||
|
// Pick the directory for images. Try `xdg-user-dir PICTURES` first.
|
||||||
|
std::string target_directory = system_get("xdg-user-dir PICTURES");
|
||||||
|
|
||||||
|
// Make sure there are no newlines.
|
||||||
|
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\n'), target_directory.end());
|
||||||
|
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\r'), target_directory.end());
|
||||||
|
|
||||||
|
// Fall back on the HOME directory if necessary.
|
||||||
|
if(target_directory.empty()) target_directory = getenv("HOME");
|
||||||
|
|
||||||
|
// Find the first available name of the form ~/clk-screenshot-<number>.bmp.
|
||||||
|
size_t index = 0;
|
||||||
|
std::string target;
|
||||||
|
while(true) {
|
||||||
|
target = target_directory + "/clk-screenshot-" + std::to_string(index) + ".bmp";
|
||||||
|
|
||||||
|
struct stat file_stats;
|
||||||
|
if(stat(target.c_str(), &file_stats))
|
||||||
|
break;
|
||||||
|
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a suitable SDL surface and save the thing.
|
||||||
|
const bool is_big_endian = SDL_BYTEORDER == SDL_BIG_ENDIAN;
|
||||||
|
SDL_Surface *const surface = SDL_CreateRGBSurfaceFrom(
|
||||||
|
screenshot.pixel_data.data(),
|
||||||
|
screenshot.width, screenshot.height,
|
||||||
|
8*4,
|
||||||
|
screenshot.width*4,
|
||||||
|
is_big_endian ? 0xff000000 : 0x000000ff,
|
||||||
|
is_big_endian ? 0x00ff0000 : 0x0000ff00,
|
||||||
|
is_big_endian ? 0x0000ff00 : 0x00ff0000,
|
||||||
|
0);
|
||||||
|
SDL_SaveBMP(surface, target.c_str());
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
|
||||||
|
// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
|
||||||
|
// systems so key up is safer.
|
||||||
|
if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
|
||||||
|
fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
|
SDL_SetWindowFullscreen(window, fullscreen_mode);
|
||||||
|
SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
|
||||||
|
|
||||||
|
// Announce a potential discontinuity in keyboard input.
|
||||||
|
const auto keyboard_machine = machine->keyboard_machine();
|
||||||
|
if(keyboard_machine) {
|
||||||
|
keyboard_machine->get_keyboard().reset_all_keys();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ctrl+escape to release the mouse (if captured).
|
|
||||||
if(event.key.keysym.sym == SDLK_ESCAPE && (SDL_GetModState()&KMOD_CTRL)) {
|
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
|
||||||
window_titler.set_mouse_is_captured(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture ctrl+shift+d as a take-a-screenshot command.
|
|
||||||
if(event.key.keysym.sym == SDLK_d && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
|
|
||||||
// Grab the screen buffer.
|
|
||||||
Outputs::Display::OpenGL::Screenshot screenshot(4, 3);
|
|
||||||
|
|
||||||
// Pick the directory for images. Try `xdg-user-dir PICTURES` first.
|
|
||||||
std::string target_directory = system_get("xdg-user-dir PICTURES");
|
|
||||||
|
|
||||||
// Make sure there are no newlines.
|
|
||||||
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\n'), target_directory.end());
|
|
||||||
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\r'), target_directory.end());
|
|
||||||
|
|
||||||
// Fall back on the HOME directory if necessary.
|
|
||||||
if(target_directory.empty()) target_directory = getenv("HOME");
|
|
||||||
|
|
||||||
// Find the first available name of the form ~/clk-screenshot-<number>.bmp.
|
|
||||||
size_t index = 0;
|
|
||||||
std::string target;
|
|
||||||
while(true) {
|
|
||||||
target = target_directory + "/clk-screenshot-" + std::to_string(index) + ".bmp";
|
|
||||||
|
|
||||||
struct stat file_stats;
|
|
||||||
if(stat(target.c_str(), &file_stats))
|
|
||||||
break;
|
|
||||||
|
|
||||||
++index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a suitable SDL surface and save the thing.
|
|
||||||
const bool is_big_endian = SDL_BYTEORDER == SDL_BIG_ENDIAN;
|
|
||||||
SDL_Surface *const surface = SDL_CreateRGBSurfaceFrom(
|
|
||||||
screenshot.pixel_data.data(),
|
|
||||||
screenshot.width, screenshot.height,
|
|
||||||
8*4,
|
|
||||||
screenshot.width*4,
|
|
||||||
is_big_endian ? 0xff000000 : 0x000000ff,
|
|
||||||
is_big_endian ? 0x00ff0000 : 0x0000ff00,
|
|
||||||
is_big_endian ? 0x0000ff00 : 0x00ff0000,
|
|
||||||
0);
|
|
||||||
SDL_SaveBMP(surface, target.c_str());
|
|
||||||
SDL_FreeSurface(surface);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deliberate fallthrough...
|
|
||||||
case SDL_KEYUP: {
|
|
||||||
|
|
||||||
// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
|
|
||||||
// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
|
|
||||||
// systems so key up is safer.
|
|
||||||
if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
|
|
||||||
fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
|
||||||
SDL_SetWindowFullscreen(window, fullscreen_mode);
|
|
||||||
SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
|
|
||||||
|
|
||||||
// Announce a potential discontinuity in keyboard input.
|
|
||||||
const auto keyboard_machine = machine->keyboard_machine();
|
|
||||||
if(keyboard_machine) {
|
|
||||||
keyboard_machine->get_keyboard().reset_all_keys();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool is_pressed = event.type == SDL_KEYDOWN;
|
const bool is_pressed = event.type == SDL_KEYDOWN;
|
||||||
|
|
||||||
const auto keyboard_machine = machine->keyboard_machine();
|
|
||||||
if(keyboard_machine) {
|
if(keyboard_machine) {
|
||||||
Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space;
|
Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space;
|
||||||
if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break;
|
if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break;
|
||||||
@ -760,12 +799,13 @@ int main(int argc, char *argv[]) {
|
|||||||
} break;
|
} break;
|
||||||
|
|
||||||
case SDL_MOUSEBUTTONDOWN:
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
if(uses_mouse && !SDL_GetRelativeMouseMode()) {
|
case SDL_MOUSEBUTTONUP: {
|
||||||
|
if(uses_mouse && event.type == SDL_MOUSEBUTTONDOWN && !SDL_GetRelativeMouseMode()) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
window_titler.set_mouse_is_captured(true);
|
window_titler.set_mouse_is_captured(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SDL_MOUSEBUTTONUP: {
|
|
||||||
const auto mouse_machine = machine->mouse_machine();
|
const auto mouse_machine = machine->mouse_machine();
|
||||||
if(mouse_machine) {
|
if(mouse_machine) {
|
||||||
mouse_machine->get_mouse().set_button_pressed(
|
mouse_machine->get_mouse().set_button_pressed(
|
||||||
@ -834,16 +874,10 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display a new frame and wait for vsync.
|
|
||||||
updater.update();
|
|
||||||
scan_target.update(int(window_width), int(window_height));
|
|
||||||
scan_target.draw(int(window_width), int(window_height));
|
|
||||||
if(activity_observer) activity_observer->draw();
|
|
||||||
SDL_GL_SwapWindow(window);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up.
|
// Clean up.
|
||||||
|
updater.flush(); // Ensure no further updates will occur.
|
||||||
joysticks.clear();
|
joysticks.clear();
|
||||||
SDL_DestroyWindow( window );
|
SDL_DestroyWindow( window );
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
@ -35,7 +35,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implemented as per Speaker.
|
// Implemented as per Speaker.
|
||||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) {
|
float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
|
||||||
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
|
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
|
||||||
|
|
||||||
// return twice the cut off, if applicable
|
// return twice the cut off, if applicable
|
||||||
@ -58,7 +58,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implemented as per Speaker.
|
// Implemented as per Speaker.
|
||||||
void set_output_rate(float cycles_per_second, int buffer_size) {
|
void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
|
||||||
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
|
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
|
||||||
filter_parameters_.output_cycles_per_second = cycles_per_second;
|
filter_parameters_.output_cycles_per_second = cycles_per_second;
|
||||||
filter_parameters_.parameters_are_dirty = true;
|
filter_parameters_.parameters_are_dirty = true;
|
||||||
|
@ -24,7 +24,15 @@ class Speaker {
|
|||||||
virtual ~Speaker() {}
|
virtual ~Speaker() {}
|
||||||
|
|
||||||
virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0;
|
virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0;
|
||||||
virtual void set_output_rate(float cycles_per_second, int buffer_size) = 0;
|
void set_output_rate(float cycles_per_second, int buffer_size) {
|
||||||
|
output_cycles_per_second_ = cycles_per_second;
|
||||||
|
output_buffer_size_ = buffer_size;
|
||||||
|
compute_output_rate();
|
||||||
|
}
|
||||||
|
void set_input_rate_multiplier(float multiplier) {
|
||||||
|
input_rate_multiplier_ = multiplier;
|
||||||
|
compute_output_rate();
|
||||||
|
}
|
||||||
|
|
||||||
int completed_sample_sets() const { return completed_sample_sets_; }
|
int completed_sample_sets() const { return completed_sample_sets_; }
|
||||||
|
|
||||||
@ -36,13 +44,26 @@ class Speaker {
|
|||||||
delegate_ = delegate;
|
delegate_ = delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void set_computed_output_rate(float cycles_per_second, int buffer_size) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) {
|
void did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) {
|
||||||
++completed_sample_sets_;
|
++completed_sample_sets_;
|
||||||
delegate_->speaker_did_complete_samples(this, buffer);
|
delegate_->speaker_did_complete_samples(this, buffer);
|
||||||
}
|
}
|
||||||
Delegate *delegate_ = nullptr;
|
Delegate *delegate_ = nullptr;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void compute_output_rate() {
|
||||||
|
// The input rate multiplier is actually used as an output rate divider,
|
||||||
|
// to confirm to the public interface of a generic speaker being output-centric.
|
||||||
|
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_);
|
||||||
|
}
|
||||||
|
|
||||||
int completed_sample_sets_ = 0;
|
int completed_sample_sets_ = 0;
|
||||||
|
float input_rate_multiplier_ = 1.0f;
|
||||||
|
float output_cycles_per_second_ = 1.0f;
|
||||||
|
int output_buffer_size_ = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user