diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp index c5a8fa724..a6f412ca4 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp @@ -37,9 +37,9 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum) return ideal / static_cast(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_) { - speaker->set_output_rate(cycles_per_second, buffer_size); + speaker->set_computed_output_rate(cycles_per_second, buffer_size); } } diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp index db660dd26..1dcf74736 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp @@ -38,9 +38,9 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: void set_new_front_machine(::Machine::DynamicMachine *machine); // Below is the standard Outputs::Speaker::Speaker interface; see there for documentation. - float get_ideal_clock_rate_in_range(float minimum, float maximum) final; - void set_output_rate(float cycles_per_second, int buffer_size) final; - void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) final; + float get_ideal_clock_rate_in_range(float minimum, float maximum) override; + void set_computed_output_rate(float cycles_per_second, int buffer_size) override; + void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override; private: void speaker_did_complete_samples(Speaker *speaker, const std::vector &buffer) final; diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 80b74343a..381ed00eb 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -337,7 +337,7 @@ class CRTCBusHandler { /// @returns The current scan status. 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. diff --git a/Machines/Apple/Macintosh/Macintosh.cpp b/Machines/Apple/Macintosh/Macintosh.cpp index be72fb477..d15b800e9 100644 --- a/Machines/Apple/Macintosh/Macintosh.cpp +++ b/Machines/Apple/Macintosh/Macintosh.cpp @@ -399,7 +399,8 @@ template class ConcreteMachin via_.flush(); audio_.queue.perform(); - // Experimental? + // This avoids deferring IWM costs indefinitely, until + // they become artbitrarily large. iwm_.flush(); } diff --git a/Machines/CRTMachine.hpp b/Machines/CRTMachine.hpp index 62c1ff408..1e57575c1 100644 --- a/Machines/CRTMachine.hpp +++ b/Machines/CRTMachine.hpp @@ -54,11 +54,31 @@ class Machine { /// Runs the machine for @c duration seconds. 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); run_for(Cycles(static_cast(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. @@ -169,6 +189,7 @@ class Machine { private: double clock_rate_ = 1.0; double clock_conversion_error_ = 0.0; + double speed_multiplier_ = 1.0; }; } diff --git a/Machines/ZX8081/Video.cpp b/Machines/ZX8081/Video.cpp index 10507a864..e5241b924 100644 --- a/Machines/ZX8081/Video.cpp +++ b/Machines/ZX8081/Video.cpp @@ -111,5 +111,5 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) { } 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; } diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 6fba9e0e8..feced1631 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -158,6 +158,8 @@ 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; }; 4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.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 */; }; 4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; }; 4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; }; @@ -4252,6 +4254,7 @@ 4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */, 4B055AB41FAE860F0060FFFF /* OricTAP.cpp in Sources */, 4B055AB71FAE860F0060FFFF /* TZX.cpp in Sources */, + 4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */, 4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */, 4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */, 4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */, @@ -4303,6 +4306,7 @@ 4BEBFB4E2002C4BF000708CC /* MSXDSK.cpp in Sources */, 4B055ADD1FAE9B460060FFFF /* i8272.cpp in Sources */, 4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */, + 4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */, 4B0ACC2723775819008902D0 /* AtariST.cpp in Sources */, 4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */, 4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */, @@ -5075,6 +5079,7 @@ GCC_WARN_UNUSED_LABEL = YES; INFOPLIST_FILE = "Clock Signal/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-Wreorder", @@ -5121,6 +5126,7 @@ GCC_WARN_UNUSED_LABEL = YES; INFOPLIST_FILE = "Clock Signal/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-Wreorder", diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index 283f6e19b..c1a57dee0 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -17,6 +17,7 @@ @implementation CSOpenGLView { CVDisplayLinkRef _displayLink; CGSize _backingSize; + NSScreen *_currentScreen; NSTrackingArea *_mouseTrackingArea; NSTimer *_mouseHideTimer; @@ -26,12 +27,38 @@ - (void)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; [[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 - CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); + NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"]; + CVDisplayLinkCreateWithCGDisplay(screenNumber.unsignedIntValue, &_displayLink); // Set the renderer output callback function CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self)); @@ -41,21 +68,44 @@ 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)]; + /* + 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; } - (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]; } @@ -75,7 +125,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt } - (void)dealloc { - // Release the display link + // Stop and release the display link + CVDisplayLinkStop(_displayLink); CVDisplayLinkRelease(_displayLink); } diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index cf6bd79a5..55125f087 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -34,10 +34,12 @@ namespace { 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 lock_guard(*machine_mutex); return machine->crt_machine()->run_until(duration, flags); } + std::mutex *machine_mutex; Machine::DynamicMachine *machine; }; @@ -103,6 +105,7 @@ class ActivityObserver: public Activity::Observer { } void set_aspect_ratio(float aspect_ratio) { + std::lock_guard lock_guard(mutex); lights_.clear(); // Generate a bunch of LEDs for connected drives. @@ -129,6 +132,7 @@ class ActivityObserver: public Activity::Observer { } void draw() { + std::lock_guard lock_guard(mutex); for(const auto &lit_led: lit_leds_) { 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); @@ -139,26 +143,31 @@ class ActivityObserver: public Activity::Observer { private: std::vector leds_; void register_led(const std::string &name) final { + std::lock_guard lock_guard(mutex); leds_.push_back(name); } std::vector drives_; void register_drive(const std::string &name) final { + std::lock_guard lock_guard(mutex); drives_.push_back(name); } void set_led_status(const std::string &name, bool lit) final { + std::lock_guard lock_guard(mutex); if(lit) lit_leds_.insert(name); else lit_leds_.erase(name); } void announce_drive_event(const std::string &name, DriveEvent event) final { + std::lock_guard lock_guard(mutex); blinking_leds_.insert(name); } std::map> lights_; std::set lit_leds_; std::set blinking_leds_; + std::mutex mutex; }; 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); // 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. 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/" }; 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() != '/') { paths.push_back(user_path + "/"); } else { @@ -442,6 +451,7 @@ int main(int argc, char *argv[]) { // Create and configure a machine. ::Machine::Error error; + std::mutex machine_mutex; std::unique_ptr<::Machine::DynamicMachine> machine(::Machine::MachineForTargets(targets, rom_fetcher, error)); if(!machine) { switch(error) { @@ -462,7 +472,25 @@ int main(int argc, char *argv[]) { 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_mutex = &machine_mutex; speaker_delegate.updater = &updater; updater.set_delegate(&best_effort_updater_delegate); @@ -617,7 +645,17 @@ int main(int argc, char *argv[]) { bool should_quit = false; Uint32 fullscreen_mode = 0; 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 lock_guard(machine_mutex); SDL_Event event; while(SDL_PollEvent(&event)) { switch(event.type) { @@ -643,87 +681,88 @@ int main(int argc, char *argv[]) { } break; case SDL_KEYDOWN: - // 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)) { - const auto keyboard_machine = machine->keyboard_machine(); - if(keyboard_machine) { - keyboard_machine->type_string(SDL_GetClipboardText()); + case SDL_KEYUP: { + const auto keyboard_machine = machine->keyboard_machine(); + + if(event.type == SDL_KEYDOWN) { + // 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-.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; } } - // 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-.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 auto keyboard_machine = machine->keyboard_machine(); if(keyboard_machine) { Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space; if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break; @@ -760,12 +799,13 @@ int main(int argc, char *argv[]) { } break; case SDL_MOUSEBUTTONDOWN: - if(uses_mouse && !SDL_GetRelativeMouseMode()) { + case SDL_MOUSEBUTTONUP: { + if(uses_mouse && event.type == SDL_MOUSEBUTTONDOWN && !SDL_GetRelativeMouseMode()) { SDL_SetRelativeMouseMode(SDL_TRUE); window_titler.set_mouse_is_captured(true); break; } - case SDL_MOUSEBUTTONUP: { + const auto mouse_machine = machine->mouse_machine(); if(mouse_machine) { 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. + updater.flush(); // Ensure no further updates will occur. joysticks.clear(); SDL_DestroyWindow( window ); SDL_Quit(); diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index ced763a56..686e00d25 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -35,7 +35,7 @@ template class LowpassSpeaker: public 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 lock_guard(filter_parameters_mutex_); // return twice the cut off, if applicable @@ -58,7 +58,7 @@ template class LowpassSpeaker: public 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 lock_guard(filter_parameters_mutex_); filter_parameters_.output_cycles_per_second = cycles_per_second; filter_parameters_.parameters_are_dirty = true; diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp index 2f575880b..941621729 100644 --- a/Outputs/Speaker/Speaker.hpp +++ b/Outputs/Speaker/Speaker.hpp @@ -24,7 +24,15 @@ class Speaker { virtual ~Speaker() {} 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_; } @@ -36,13 +44,26 @@ class Speaker { delegate_ = delegate; } + virtual void set_computed_output_rate(float cycles_per_second, int buffer_size) = 0; + protected: void did_complete_samples(Speaker *speaker, const std::vector &buffer) { ++completed_sample_sets_; delegate_->speaker_did_complete_samples(this, buffer); } 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; + float input_rate_multiplier_ = 1.0f; + float output_cycles_per_second_ = 1.0f; + int output_buffer_size_ = 1; }; }