1
0
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:
Thomas Harte 2020-01-26 23:42:25 -05:00
commit 7316a3aa88
11 changed files with 241 additions and 107 deletions

View File

@ -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);
} }
} }

View File

@ -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;

View File

@ -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.

View File

@ -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();
} }

View File

@ -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;
}; };
} }

View File

@ -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;
} }

View File

@ -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",

View File

@ -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);
} }

View File

@ -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();

View File

@ -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;

View File

@ -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;
}; };
} }