// // main.cpp // Clock Signal // // Created by Thomas Harte on 04/11/2017. // Copyright 2017 Thomas Harte. All rights reserved. // #include #include #include #include #include #include #include #include #include #include "../../Analyser/Static/StaticAnalyser.hpp" #include "../../Machines/Utility/MachineForTarget.hpp" #include "../../Machines/MediaTarget.hpp" #include "../../Machines/CRTMachine.hpp" #include "../../Concurrency/BestEffortUpdater.hpp" #include "../../Activity/Observer.hpp" #include "../../Outputs/CRT/Internals/Rectangle.hpp" namespace { struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate { void update(Concurrency::BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update) override { machine->crt_machine()->run_for(duration); } Machine::DynamicMachine *machine; }; struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate { // This is set to a relatively large number for now. static const int buffer_size = 1024; void speaker_did_complete_samples(Outputs::Speaker::Speaker *speaker, const std::vector &buffer) override { std::lock_guard lock_guard(audio_buffer_mutex_); if(audio_buffer_.size() > buffer_size) { audio_buffer_.erase(audio_buffer_.begin(), audio_buffer_.end() - buffer_size); } audio_buffer_.insert(audio_buffer_.end(), buffer.begin(), buffer.end()); } void audio_callback(Uint8 *stream, int len) { updater->update(); std::lock_guard lock_guard(audio_buffer_mutex_); std::size_t sample_length = static_cast(len) / sizeof(int16_t); std::size_t copy_length = std::min(sample_length, audio_buffer_.size()); int16_t *target = static_cast(static_cast(stream)); std::memcpy(stream, audio_buffer_.data(), copy_length * sizeof(int16_t)); if(copy_length < sample_length) { std::memset(&target[copy_length], 0, (sample_length - copy_length) * sizeof(int16_t)); } audio_buffer_.erase(audio_buffer_.begin(), audio_buffer_.begin() + copy_length); } static void SDL_audio_callback(void *userdata, Uint8 *stream, int len) { reinterpret_cast(userdata)->audio_callback(stream, len); } SDL_AudioDeviceID audio_device; Concurrency::BestEffortUpdater *updater; std::mutex audio_buffer_mutex_; std::vector audio_buffer_; }; class ActivityObserver: public Activity::Observer { public: ActivityObserver(Activity::Source *source, float aspect_ratio) { // Get the suorce to supply all LEDs and drives. source->set_activity_observer(this); // The objective is to display drives on one side of the screen, other LEDs on the other. Drives // may or may not have LEDs and this code intends to display only those which do; so a quick // comparative processing of the two lists is called for. // Strip the list of drives to only those which have LEDs. Thwy're the ones that'll be displayed. drives_.resize(std::remove_if(drives_.begin(), drives_.end(), [this](const std::string &string) { return std::find(leds_.begin(), leds_.end(), string) == leds_.end(); }) - drives_.begin()); // Remove from the list of LEDs any which are drives. Those will be represented separately. leds_.resize(std::remove_if(leds_.begin(), leds_.end(), [this](const std::string &string) { return std::find(drives_.begin(), drives_.end(), string) != drives_.end(); }) - leds_.begin()); set_aspect_ratio(aspect_ratio); } void set_aspect_ratio(float aspect_ratio) { lights_.clear(); // Generate a bunch of LEDs for connected drives. const float height = 0.05f; const float width = height / aspect_ratio; const float right_x = 1.0f - 2.0f * width; float y = 1.0f - 2.0f * height; for(const auto &drive: drives_) { // TODO: use std::make_unique as below, if/when formally embracing C++14. lights_.emplace(std::make_pair(drive, std::unique_ptr(new OpenGL::Rectangle(right_x, y, width, height)))); y -= height * 2.0f; } /* This would generate LEDs for things other than drives; I'm declining for now due to the inexpressiveness of just painting a rectangle. const float left_x = -1.0f + 2.0f * width; y = 1.0f - 2.0f * height; for(const auto &led: leds_) { lights_.emplace(std::make_pair(led, std::make_unique(left_x, y, width, height))); y -= height * 2.0f; } */ } void draw() { 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); } blinking_leds_.clear(); } private: std::vector leds_; void register_led(const std::string &name) override { leds_.push_back(name); } std::vector drives_; void register_drive(const std::string &name) override { drives_.push_back(name); } void set_led_status(const std::string &name, bool lit) override { if(lit) lit_leds_.insert(name); else lit_leds_.erase(name); } void announce_drive_event(const std::string &name, DriveEvent event) override { blinking_leds_.insert(name); } std::map> lights_; std::set lit_leds_; std::set blinking_leds_; }; bool KeyboardKeyForSDLScancode(SDL_Keycode scancode, Inputs::Keyboard::Key &key) { #define BIND(x, y) case SDL_SCANCODE_##x: key = Inputs::Keyboard::Key::y; break; switch(scancode) { default: return false; BIND(F1, F1) BIND(F2, F2) BIND(F3, F3) BIND(F4, F4) BIND(F5, F5) BIND(F6, F6) BIND(F7, F7) BIND(F8, F8) BIND(F9, F9) BIND(F10, F10) BIND(F11, F11) BIND(F12, F12) BIND(1, k1) BIND(2, k2) BIND(3, k3) BIND(4, k4) BIND(5, k5) BIND(6, k6) BIND(7, k7) BIND(8, k8) BIND(9, k9) BIND(0, k0) BIND(Q, Q) BIND(W, W) BIND(E, E) BIND(R, R) BIND(T, T) BIND(Y, Y) BIND(U, U) BIND(I, I) BIND(O, O) BIND(P, P) BIND(A, A) BIND(S, S) BIND(D, D) BIND(F, F) BIND(G, G) BIND(H, H) BIND(J, J) BIND(K, K) BIND(L, L) BIND(Z, Z) BIND(X, X) BIND(C, C) BIND(V, V) BIND(B, B) BIND(N, N) BIND(M, M) BIND(KP_7, KeyPad7) BIND(KP_8, KeyPad8) BIND(KP_9, KeyPad9) BIND(KP_4, KeyPad4) BIND(KP_5, KeyPad5) BIND(KP_6, KeyPad6) BIND(KP_1, KeyPad1) BIND(KP_2, KeyPad2) BIND(KP_3, KeyPad3) BIND(KP_0, KeyPad0) BIND(ESCAPE, Escape) BIND(PRINTSCREEN, PrintScreen) BIND(SCROLLLOCK, ScrollLock) BIND(PAUSE, Pause) BIND(GRAVE, BackTick) BIND(MINUS, Hyphen) BIND(EQUALS, Equals) BIND(BACKSPACE, BackSpace) BIND(TAB, Tab) BIND(LEFTBRACKET, OpenSquareBracket) BIND(RIGHTBRACKET, CloseSquareBracket) BIND(BACKSLASH, BackSlash) BIND(CAPSLOCK, CapsLock) BIND(SEMICOLON, Semicolon) BIND(APOSTROPHE, Quote) BIND(RETURN, Enter) BIND(LSHIFT, LeftShift) BIND(COMMA, Comma) BIND(PERIOD, FullStop) BIND(SLASH, ForwardSlash) BIND(RSHIFT, RightShift) BIND(LCTRL, LeftControl) BIND(LALT, LeftOption) BIND(LGUI, LeftMeta) BIND(SPACE, Space) BIND(RCTRL, RightControl) BIND(RALT, RightOption) BIND(RGUI, RightMeta) BIND(LEFT, Left) BIND(RIGHT, Right) BIND(UP, Up) BIND(DOWN, Down) BIND(INSERT, Insert) BIND(HOME, Home) BIND(PAGEUP, PageUp) BIND(DELETE, Delete) BIND(END, End) BIND(PAGEDOWN, PageDown) BIND(NUMLOCKCLEAR, NumLock) BIND(KP_DIVIDE, KeyPadSlash) BIND(KP_MULTIPLY, KeyPadAsterisk) BIND(KP_PLUS, KeyPadPlus) BIND(KP_MINUS, KeyPadMinus) BIND(KP_ENTER, KeyPadEnter) BIND(KP_DECIMAL, KeyPadDecimalPoint) BIND(KP_EQUALS, KeyPadEquals) BIND(HELP, Help) // SDL doesn't seem to have scancodes for hash or keypad delete? } #undef BIND return true; } struct ParsedArguments { std::string file_name; Configurable::SelectionSet selections; }; /*! Parses an argc/argv pair to discern program arguments. */ ParsedArguments parse_arguments(int argc, char *argv[]) { ParsedArguments arguments; for(int index = 1; index < argc; ++index) { char *arg = argv[index]; // Accepted format is: // // --flag sets a Boolean option to true. // --flag=value sets the value for a list option. // name sets the file name to load. // Anything starting with a dash always makes a selection; otherwise it's a file name. if(arg[0] == '-') { while(*arg == '-') arg++; // Check for an equals sign, to discern a Boolean selection from a list selection. std::string argument = arg; std::size_t split_index = argument.find("="); if(split_index == std::string::npos) { arguments.selections[argument].reset(new Configurable::BooleanSelection(true)); } else { std::string name = argument.substr(0, split_index); std::string value = argument.substr(split_index+1, std::string::npos); arguments.selections[name].reset(new Configurable::ListSelection(value)); } } else { arguments.file_name = arg; } } return arguments; } std::string final_path_component(const std::string &path) { // An empty path has no final component. if(path.empty()) { return ""; } // Find the last slash... auto final_slash = path.find_last_of("/\\"); // If no slash was found at all, return the whole path. if(final_slash == std::string::npos) { return path; } // If a slash was found in the final position, remove it and recurse. if(final_slash == path.size() - 1) { return final_path_component(path.substr(0, path.size() - 1)); } // Otherwise return everything from just after the slash to the end of the path. return path.substr(final_slash+1, path.size() - final_slash - 1); } /*! Executes @c command and returns its STDOUT. */ std::string system_get(const char *command) { std::unique_ptr pipe(popen(command, "r"), pclose); if(!pipe) return ""; std::string result; while(!feof(pipe.get())) { std::array buffer; if (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) result += buffer.data(); } return result; } } int main(int argc, char *argv[]) { SDL_Window *window = nullptr; // Attempt to parse arguments. ParsedArguments arguments = parse_arguments(argc, argv); // This may be printed either as const std::string usage_suffix = " [file] [OPTIONS] [--rompath={path to ROMs}]"; // Print a help message if requested. if(arguments.selections.find("help") != arguments.selections.end() || arguments.selections.find("h") != arguments.selections.end()) { std::cout << "Usage: " << final_path_component(argv[0]) << usage_suffix << std::endl; std::cout << "Use alt+enter to toggle full screen display. Use control+shift+V to paste text." << std::endl; std::cout << "Required machine type and configuration is determined from the file. Machines with further options:" << std::endl << std::endl; auto all_options = Machine::AllOptionsByMachineName(); for(const auto &machine_options: all_options) { std::cout << machine_options.first << ":" << std::endl; for(const auto &option: machine_options.second) { std::cout << '\t' << "--" << option->short_name; Configurable::ListOption *list_option = dynamic_cast(option.get()); if(list_option) { std::cout << "={"; bool is_first = true; for(const auto &option: list_option->options) { if(!is_first) std::cout << '|'; is_first = false; std::cout << option; } std::cout << "}"; } std::cout << std::endl; } std::cout << std::endl; } return 0; } // Perform a sanity check on arguments. if(arguments.file_name.empty()) { std::cerr << "Usage: " << final_path_component(argv[0]) << usage_suffix << std::endl; std::cerr << "Use --help to learn more about available options." << std::endl; return -1; } // Determine the machine for the supplied file. Analyser::Static::TargetList targets = Analyser::Static::GetTargets(arguments.file_name); if(targets.empty()) { std::cerr << "Cannot open " << arguments.file_name << "; no target machine found" << std::endl; return -1; } Concurrency::BestEffortUpdater updater; BestEffortUpdaterDelegate best_effort_updater_delegate; SpeakerDelegate speaker_delegate; // For vanilla SDL purposes, assume system ROMs can be found in one of: // // /usr/local/share/CLK/[system]; // /usr/share/CLK/[system]; or // [user-supplied path]/[system] std::vector rom_names; std::string machine_name; ROMMachine::ROMFetcher rom_fetcher = [&rom_names, &machine_name, &arguments] (const std::string &machine, const std::vector &names) -> std::vector>> { rom_names.insert(rom_names.end(), names.begin(), names.end()); machine_name = machine; std::vector paths = { "/usr/local/share/CLK/", "/usr/share/CLK/" }; if(arguments.selections.find("rompath") != arguments.selections.end()) { std::string user_path = arguments.selections["rompath"]->list_selection()->value; if(user_path.back() != '/') { paths.push_back(user_path + "/"); } else { paths.push_back(user_path); } } std::vector>> results; for(const auto &name: names) { FILE *file = nullptr; for(const auto &path: paths) { std::string local_path = path + machine + "/" + name; file = std::fopen(local_path.c_str(), "rb"); if(file) break; } if(!file) { results.emplace_back(nullptr); continue; } std::unique_ptr> data(new std::vector); std::fseek(file, 0, SEEK_END); data->resize(std::ftell(file)); std::fseek(file, 0, SEEK_SET); std::size_t read = fread(data->data(), 1, data->size(), file); std::fclose(file); if(read == data->size()) results.emplace_back(std::move(data)); else results.emplace_back(nullptr); } return results; }; // Create and configure a machine. ::Machine::Error error; std::unique_ptr<::Machine::DynamicMachine> machine(::Machine::MachineForTargets(targets, rom_fetcher, error)); if(!machine) { switch(error) { default: break; case ::Machine::Error::MissingROM: std::cerr << "Could not find system ROMs; please install to /usr/local/share/CLK/ or /usr/share/CLK/, or provide a --rompath." << std::endl; std::cerr << "One or more of the following were needed but not found:" << std::endl; for(const auto &name: rom_names) { std::cerr << machine_name << '/' << name << std::endl; } break; } return -1; } best_effort_updater_delegate.machine = machine.get(); speaker_delegate.updater = &updater; updater.set_delegate(&best_effort_updater_delegate); // Attempt to set up video and audio. if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; return -1; } // Ask for no depth buffer, a core profile and vsync-aligned rendering. SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetSwapInterval(1); window = SDL_CreateWindow( final_path_component(arguments.file_name).c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 400, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if(!window) { std::cerr << "Could not create window" << std::endl; return -1; } SDL_GLContext gl_context = SDL_GL_CreateContext(window); SDL_GL_MakeCurrent(window, gl_context); GLint target_framebuffer = 0; glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer); // Setup output, assuming a CRT machine for now, and prepare a best-effort updater. machine->crt_machine()->setup_output(4.0 / 3.0); machine->crt_machine()->get_crt()->set_output_gamma(2.2f); machine->crt_machine()->get_crt()->set_target_framebuffer(target_framebuffer); // For now, lie about audio output intentions. auto speaker = machine->crt_machine()->get_speaker(); if(speaker) { // Create an audio pipe. SDL_AudioSpec desired_audio_spec; SDL_AudioSpec obtained_audio_spec; SDL_zero(desired_audio_spec); desired_audio_spec.freq = 48000; // TODO: how can I get SDL to reveal the output rate of this machine? desired_audio_spec.format = AUDIO_S16; desired_audio_spec.channels = 1; desired_audio_spec.samples = SpeakerDelegate::buffer_size; desired_audio_spec.callback = SpeakerDelegate::SDL_audio_callback; desired_audio_spec.userdata = &speaker_delegate; speaker_delegate.audio_device = SDL_OpenAudioDevice(nullptr, 0, &desired_audio_spec, &obtained_audio_spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); speaker->set_output_rate(obtained_audio_spec.freq, desired_audio_spec.samples); speaker->set_delegate(&speaker_delegate); SDL_PauseAudioDevice(speaker_delegate.audio_device, 0); } int window_width, window_height; SDL_GetWindowSize(window, &window_width, &window_height); Configurable::Device *const configurable_device = machine->configurable_device(); if(configurable_device) { // Establish user-friendly options by default. configurable_device->set_selections(configurable_device->get_user_friendly_selections()); // Consider transcoding any list selections that map to Boolean options. for(const auto &option: configurable_device->get_options()) { // Check for a corresponding selection. auto selection = arguments.selections.find(option->short_name); if(selection != arguments.selections.end()) { // Transcode selection if necessary. if(dynamic_cast(option.get())) { arguments.selections[selection->first] = std::unique_ptr(selection->second->boolean_selection()); } if(dynamic_cast(option.get())) { arguments.selections[selection->first] = std::unique_ptr(selection->second->list_selection()); } } } // Apply the user's actual selections to override the defaults. configurable_device->set_selections(arguments.selections); } // If this is a joystick machine, check for and open attached joysticks. /*! Provides a wrapper for SDL_Joystick pointers that can keep track of historic hat values. */ class SDLJoystick { public: SDLJoystick(SDL_Joystick *joystick) : joystick_(joystick) { hat_values_.resize(SDL_JoystickNumHats(joystick)); } ~SDLJoystick() { SDL_JoystickClose(joystick_); } /// @returns The underlying SDL_Joystick. SDL_Joystick *get() { return joystick_; } /// @returns A reference to the storage for the previous state of hat @c c. Uint8 &last_hat_value(int c) { return hat_values_[c]; } /// @returns The logic OR of all stored hat states. Uint8 hat_values() { Uint8 value = 0; for(const auto hat_value: hat_values_) { value |= hat_value; } return value; } private: SDL_Joystick *joystick_; std::vector hat_values_; }; std::vector joysticks; JoystickMachine::Machine *const joystick_machine = machine->joystick_machine(); if(joystick_machine) { SDL_InitSubSystem(SDL_INIT_JOYSTICK); for(int c = 0; c < SDL_NumJoysticks(); ++c) { joysticks.emplace_back(SDL_JoystickOpen(c)); } } /* If the machine offers anything for activity observation, create and register an activity observer. */ std::unique_ptr activity_observer; Activity::Source *const activity_source = machine->activity_source(); if(activity_source) { activity_observer.reset(new ActivityObserver(activity_source, 4.0f / 3.0f)); } // Run the main event loop until the OS tells us to quit. bool should_quit = false; Uint32 fullscreen_mode = 0; while(!should_quit) { // Process all pending events. SDL_Event event; while(SDL_PollEvent(&event)) { switch(event.type) { case SDL_QUIT: should_quit = true; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_RESIZED: { GLint target_framebuffer = 0; glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer); machine->crt_machine()->get_crt()->set_target_framebuffer(target_framebuffer); SDL_GetWindowSize(window, &window_width, &window_height); if(activity_observer) activity_observer->set_aspect_ratio(static_cast(window_width) / static_cast(window_height)); } break; default: break; } break; case SDL_DROPFILE: { Analyser::Static::Media media = Analyser::Static::GetMedia(event.drop.file); machine->media_target()->insert_media(media); } 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)) { KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine(); if(keyboard_machine) { keyboard_machine->type_string(SDL_GetClipboardText()); break; } } // 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)) { // Pick a width to capture that will preserve a 4:3 output aspect ratio. const int proportional_width = (window_height * 4) / 3; // Grab the screen buffer. std::vector pixels(proportional_width * window_height * 4); glReadPixels((window_width - proportional_width) >> 1, 0, proportional_width, window_height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); // Flip the buffer vertically, because SDL and OpenGL do not agree about // the basis axes. std::vector swap_buffer(proportional_width*4); for(int y = 0; y < window_height >> 1; ++y) { memcpy(swap_buffer.data(), &pixels[y*proportional_width*4], swap_buffer.size()); memcpy(&pixels[y*proportional_width*4], &pixels[(window_height - 1 - y)*proportional_width*4], swap_buffer.size()); memcpy(&pixels[(window_height - 1 - y)*proportional_width*4], swap_buffer.data(), swap_buffer.size()); } // Pick the directory for images. Try `xdg-user-dir PICTURES` first; failing that // fall back on the HOME directory. std::string target_directory = system_get("xdg-user-dir PICTURES"); 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( pixels.data(), proportional_width, window_height, 8*4, proportional_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. 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; KeyboardMachine::Machine *const keyboard_machine = machine->keyboard_machine(); if(keyboard_machine) { Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space; if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break; char key_value = '\0'; const char *key_name = SDL_GetKeyName(event.key.keysym.sym); if(key_name[0] >= 0) key_value = key_name[0]; keyboard_machine->get_keyboard().set_key_pressed(key, key_value, is_pressed); break; } JoystickMachine::Machine *const joystick_machine = machine->joystick_machine(); if(joystick_machine) { std::vector> &joysticks = joystick_machine->get_joysticks(); if(!joysticks.empty()) { switch(event.key.keysym.scancode) { case SDL_SCANCODE_LEFT: joysticks[0]->set_input(Inputs::Joystick::Input::Left, is_pressed); break; case SDL_SCANCODE_RIGHT: joysticks[0]->set_input(Inputs::Joystick::Input::Right, is_pressed); break; case SDL_SCANCODE_UP: joysticks[0]->set_input(Inputs::Joystick::Input::Up, is_pressed); break; case SDL_SCANCODE_DOWN: joysticks[0]->set_input(Inputs::Joystick::Input::Down, is_pressed); break; case SDL_SCANCODE_SPACE: joysticks[0]->set_input(Inputs::Joystick::Input::Fire, is_pressed); break; case SDL_SCANCODE_A: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 0), is_pressed); break; case SDL_SCANCODE_S: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 1), is_pressed); break; default: { const char *key_name = SDL_GetKeyName(event.key.keysym.sym); joysticks[0]->set_input(Inputs::Joystick::Input(key_name[0]), is_pressed); } break; } } } } break; default: break; } } // Push new joystick state, if any. JoystickMachine::Machine *const joystick_machine = machine->joystick_machine(); if(joystick_machine) { std::vector> &machine_joysticks = joystick_machine->get_joysticks(); for(size_t c = 0; c < joysticks.size(); ++c) { size_t target = c % machine_joysticks.size(); // Post the first two analogue axes presented by the controller as horizontal and vertical inputs, // unless the user seems to be using a hat. // SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0] if(!joysticks[c].hat_values()) { const float x_axis = static_cast(SDL_JoystickGetAxis(joysticks[c].get(), 0) + 32768) / 65535.0f; const float y_axis = static_cast(SDL_JoystickGetAxis(joysticks[c].get(), 1) + 32768) / 65535.0f; machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis); machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis); } // Forward hats as directions; hats always override analogue inputs. const int number_of_hats = SDL_JoystickNumHats(joysticks[c].get()); for(int hat = 0; hat < number_of_hats; ++hat) { const Uint8 hat_value = SDL_JoystickGetHat(joysticks[c].get(), hat); const Uint8 changes = hat_value ^ joysticks[c].last_hat_value(hat); joysticks[c].last_hat_value(hat) = hat_value; if(changes & SDL_HAT_UP) { machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat_value & SDL_HAT_UP)); } if(changes & SDL_HAT_DOWN) { machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat_value & SDL_HAT_DOWN)); } if(changes & SDL_HAT_LEFT) { machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat_value & SDL_HAT_LEFT)); } if(changes & SDL_HAT_RIGHT) { machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat_value & SDL_HAT_RIGHT)); } } // Forward all fire buttons, retaining their original indices. const int number_of_buttons = SDL_JoystickNumButtons(joysticks[c].get()); for(int button = 0; button < number_of_buttons; ++button) { machine_joysticks[target]->set_input( Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, button), SDL_JoystickGetButton(joysticks[c].get(), button) ? true : false); } } } // Display a new frame and wait for vsync. updater.update(); machine->crt_machine()->get_crt()->draw_frame(static_cast(window_width), static_cast(window_height), false); if(activity_observer) activity_observer->draw(); SDL_GL_SwapWindow(window); } // Clean up. joysticks.clear(); SDL_DestroyWindow( window ); SDL_Quit(); return 0; }