├── demo.gif ├── .gitignore ├── .gitmodules ├── .clang-format ├── src ├── wsg.h ├── pac.h ├── wsg.c ├── main.c └── pac.c ├── LICENSE ├── CMakeLists.txt └── README.md /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superzazu/pac/HEAD/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | build/ 4 | build-web/ 5 | roms/ 6 | TODO.md 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "z80"] 2 | path = deps/z80 3 | url = https://github.com/superzazu/z80.git 4 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: llvm 2 | SortIncludes: false 3 | PointerAlignment: Left 4 | AllowShortIfStatementsOnASingleLine: false 5 | AllowShortFunctionsOnASingleLine: false 6 | AllowShortLoopsOnASingleLine: false 7 | AllowShortCaseLabelsOnASingleLine: true 8 | AllowShortBlocksOnASingleLine: false 9 | AlignEscapedNewlines: DontAlign 10 | AlignAfterOpenBracket: DontAlign 11 | SpaceAfterCStyleCast: true 12 | AlignTrailingComments: false 13 | -------------------------------------------------------------------------------- /src/wsg.h: -------------------------------------------------------------------------------- 1 | #ifndef PAC_WSG_H 2 | #define PAC_WSG_H 3 | // Namco WSG (Waveform Sound Generator): 3 voice mono wavetable chip (96kHz) 4 | 5 | #include 6 | 7 | #define WSG_SAMPLE_RATE 96000 8 | 9 | typedef struct wsg_voice { 10 | uint32_t frequency; // 20 bit value 11 | uint32_t accumulator; // 20 bit value 12 | uint8_t waveform_no; // 3 bit value 13 | uint8_t volume; // 4 bit value 14 | } wsg_voice; 15 | 16 | typedef struct wsg { 17 | wsg_voice voices[3]; 18 | uint8_t* sound_rom; 19 | int gain; 20 | } wsg; 21 | 22 | void wsg_init(wsg* const w, uint8_t* const sound_rom); 23 | void wsg_write(wsg* const w, uint8_t address, uint8_t value); 24 | void wsg_play(wsg* const w, int16_t* const buffer, int buffer_len); 25 | 26 | #endif // PAC_WSG_H 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nicolas Allemand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pac.h: -------------------------------------------------------------------------------- 1 | #ifndef PAC_PAC_H 2 | #define PAC_PAC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "z80/z80.h" 10 | #include "wsg.h" 11 | 12 | #define PAC_CLOCK_SPEED 3072000 // 3.072 MHz (= number of cycles per second) 13 | #define PAC_FPS 60 14 | #define PAC_CYCLES_PER_FRAME (PAC_CLOCK_SPEED / PAC_FPS) 15 | #define PAC_SCREEN_WIDTH 224 16 | #define PAC_SCREEN_HEIGHT 288 17 | 18 | typedef struct pac pac; 19 | struct pac { 20 | z80 cpu; 21 | uint8_t rom[0x10000]; // 0x0000-0x4000 22 | uint8_t ram[0x1000]; // 0x4000-0x5000 23 | uint8_t sprite_pos[0x10]; // 0x5060-0x506f 24 | 25 | uint8_t color_rom[32]; 26 | uint8_t palette_rom[0x100]; 27 | uint8_t tile_rom[0x1000]; 28 | uint8_t sprite_rom[0x1000]; 29 | uint8_t sound_rom1[0x100]; 30 | uint8_t sound_rom2[0x100]; 31 | 32 | uint8_t tiles[256 * 8 * 8]; // to store predecoded tiles 33 | uint8_t sprites[64 * 16 * 16]; // to store predecoded sprites 34 | 35 | uint8_t int_vector; 36 | bool vblank_enabled; 37 | bool sound_enabled; 38 | bool flip_screen; 39 | 40 | // in 0 port 41 | bool p1_up, p1_left, p1_right, p1_down, rack_advance, coin_s1, coin_s2, 42 | credits_btn; 43 | 44 | // in 1 port 45 | bool board_test, p1_start, p2_start; 46 | 47 | // ppu 48 | uint8_t screen_buffer[PAC_SCREEN_HEIGHT * PAC_SCREEN_WIDTH * 3]; 49 | void (*update_screen)(pac* const n); 50 | 51 | // audio 52 | wsg sound_chip; 53 | int audio_buffer_len; 54 | int16_t* audio_buffer; 55 | int sample_rate; 56 | bool mute_audio; 57 | void (*push_sample)(pac* const n, int16_t); 58 | }; 59 | 60 | int pac_init(pac* const p, const char* rom_dir); 61 | void pac_quit(pac* const p); 62 | void pac_update(pac* const p, unsigned int ms); 63 | 64 | void pac_cheat_invincibility(pac* const p); 65 | 66 | #endif // PAC_PAC_H 67 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | 3 | project(pac 4 | LANGUAGES C 5 | DESCRIPTION "A Pac-Man emulator written in C99 with the SDL2." 6 | HOMEPAGE_URL "https://github.com/superzazu/pac" 7 | ) 8 | 9 | set(SOURCES src/main.c src/pac.c src/wsg.c deps/z80/z80.c) 10 | set(ROMS_DIR "./roms/" CACHE STRING "Path to directory containing rom files") 11 | 12 | add_executable(pac ${SOURCES}) 13 | set_target_properties(pac PROPERTIES C_STANDARD 99) 14 | if (MSVC) 15 | target_compile_options(pac PRIVATE /W4) 16 | else() 17 | target_compile_options(pac PRIVATE -Wall -Wextra -pedantic) 18 | endif() 19 | 20 | if (EMSCRIPTEN) 21 | set(CMAKE_EXECUTABLE_SUFFIX ".html") 22 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -s USE_SDL=2") 23 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --preload-file ${ROMS_DIR}@/") 24 | else() 25 | find_package(SDL2 REQUIRED) 26 | target_include_directories(pac PRIVATE ${SDL2_INCLUDE_DIRS}) 27 | target_link_libraries(pac PRIVATE ${SDL2_LIBRARIES}) 28 | endif() 29 | 30 | target_include_directories(pac PRIVATE src/ deps/) 31 | 32 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" AND NOT EMSCRIPTEN) 33 | set_target_properties(pac PROPERTIES 34 | MACOSX_BUNDLE TRUE 35 | MACOSX_BUNDLE_BUNDLE_NAME "pac" 36 | MACOSX_BUNDLE_BUNDLE_VERSION "1.0.0" 37 | MACOSX_BUNDLE_COPYRIGHT "superzazu" 38 | MACOSX_BUNDLE_GUI_IDENTIFIER "com.nicolasallemand.pac" 39 | ) 40 | 41 | file(COPY ${ROMS_DIR} DESTINATION 42 | "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.app/Contents/Resources") 43 | 44 | # add NSHighResolutionCapable property in Info.plist for Retina screens 45 | add_custom_command(TARGET pac POST_BUILD 46 | COMMAND plutil -replace NSHighResolutionCapable -bool true 47 | "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.app/Contents/Info.plist" 48 | ) 49 | 50 | # fixup the bundle to make it distributable 51 | install(CODE " 52 | include(BundleUtilities) 53 | set(BU_CHMOD_BUNDLE_ITEMS TRUE) 54 | fixup_bundle(${CMAKE_BINARY_DIR}/${PROJECT_NAME}.app \"\" \"\") 55 | " COMPONENT Runtime) 56 | endif() 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pac 2 | 3 | A [Pac-Man (1980 arcade game)](https://en.wikipedia.org/wiki/Pac-Man) emulator written in C99 with the SDL2. 4 | 5 | ![pac demo gif](demo.gif) 6 | 7 | Features: 8 | 9 | - [x] full emulation 10 | - [x] sound emulation 11 | - [x] two-player mode 12 | - [x] joystick support 13 | - [x] web export to HTML5 using emscripten 14 | 15 | The emulator has been tested with Midway Pac-Man (North American ver.): 16 | 17 | ``` 18 | SHA256(82s123.7f)= 48fe0b01d68e3d702019ca715f7266c8e3261c769509b281720f53ca0a1cc8fb 19 | SHA256(82s126.1m)= 8e723ad91e46ef1a186b2ed3c99a8bf1c571786bc7ceae2b367cbfc80857a394 20 | SHA256(82s126.3m)= 8c34002652e587aa19a77bff9040d870af18b4b2fe5c5f0ed962899386e0e751 21 | SHA256(82s126.4a)= ef8f7a3b0c10f787d9cc1cbc5cc266fcc1afadb24c3b4d610fe252b9c3df1d76 22 | SHA256(pacman.5e)= 8d9a86c97fe94b1fd010b139672c330e3b257ba59b0d8df7a821592e30a77b4b 23 | SHA256(pacman.5f)= 49c8f656cb8ea1ae02fb64a2c09df98e7f06a034b43c6c8240032df417c6d36f 24 | SHA256(pacman.6e)= fe1c3234df345855d30728637f361f79472cabfe2a892a7567c63eaf31a4217b 25 | SHA256(pacman.6f)= 09a723c9f84790e9019633e37761cfa4e9d7ab6db14f6fdb12738f51fec11065 26 | SHA256(pacman.6h)= 69347409739b64ed9d9b19713de0bc66627bd137687de649796b9d2ef88ed8e6 27 | SHA256(pacman.6j)= 03ee523c210e87fb8dd1d925b092ad269fdd753b5b7a20b3757b0ceee5f18679 28 | ``` 29 | 30 | ## How to build 31 | 32 | You should be able to launch it by running: 33 | 34 | ```bash 35 | mkdir build 36 | cd build 37 | cmake .. 38 | make 39 | ./pac ./roms/ 40 | ``` 41 | 42 | It has been tested on macOS 10.15 with clang and debian 10 with gcc 8.3. 43 | 44 | You can also build it for the web if you have emscripten installed. Just run `emcmake cmake .. -DROMS_DIR=../roms && emmake make` to obtain four "pac.(js|wasm|data|html)" files which can be hosted on a web server. 45 | 46 | ## How to play 47 | 48 | | Computer | pac | 49 | | ----------- | ----------------------------- | 50 | | 1 or c | insert a coin | 51 | | 5 or return | start a game (1 player) | 52 | | 2 | start a game (2 players) | 53 | | arrow keys | move the player | 54 | | p | pause the emulation | 55 | | m | mute audio | 56 | | s | take a screenshot | 57 | | t | run the board test | 58 | | tab | run the emulation at x5 speed | 59 | 60 | ## Resources 61 | 62 | - https://www.lomont.org/software/games/pacman/PacmanEmulation.pdf 63 | - https://www.arcade-history.com/?n=pac-man-model-932&page=detail&id=1914 64 | - http://www.arcaderestoration.com/memorymap/6365/Pac-Man.aspx 65 | - http://umlautllama.com/projects/pacdocs/ 66 | - https://simonowen.com/articles/pacemu/ 67 | -------------------------------------------------------------------------------- /src/wsg.c: -------------------------------------------------------------------------------- 1 | #include "wsg.h" 2 | 3 | void wsg_init(wsg* const w, uint8_t* const sound_rom) { 4 | for (int voice_no = 0; voice_no < 3; voice_no++) { 5 | w->voices[voice_no].frequency = 0; 6 | w->voices[voice_no].accumulator = 0; 7 | w->voices[voice_no].waveform_no = 0; 8 | w->voices[voice_no].volume = 0; 9 | } 10 | w->sound_rom = sound_rom; 11 | w->gain = 25; 12 | } 13 | 14 | void wsg_write(wsg* const w, uint8_t address, uint8_t value) { 15 | // waveform 1 16 | if (address == 0x5) { 17 | w->voices[0].waveform_no = value & 0b111; 18 | } else if (address >= 0x10 && address <= 0x14) { 19 | uint8_t sample_no = address - 0x10; 20 | w->voices[0].frequency &= ~(0b1111 << (sample_no * 4)); 21 | w->voices[0].frequency |= (value & 0b1111) << (sample_no * 4); 22 | } else if (address == 0x15) { 23 | w->voices[0].volume = value & 0xf; 24 | } else if (address >= 0 && address <= 0x4) { 25 | uint8_t sample_no = address - 0; 26 | w->voices[0].accumulator &= ~(0b1111 << (sample_no * 4)); 27 | w->voices[0].accumulator |= (value & 0b1111) << (sample_no * 4); 28 | } 29 | 30 | // waveform 2 31 | else if (address == 0xa) { 32 | w->voices[1].waveform_no = value & 0b111; 33 | } else if (address >= 0x16 && address <= 0x19) { 34 | // voice 2 and 3 cannot set lowest 4 bits of frequency 35 | uint8_t sample_no = address - 0x16 + 1; 36 | w->voices[1].frequency &= ~(0b1111 << (sample_no * 4)); 37 | w->voices[1].frequency |= (value & 0b1111) << (sample_no * 4); 38 | } else if (address == 0x1a) { 39 | w->voices[1].volume = value & 0xf; 40 | } else if (address >= 0x6 && address <= 0x9) { 41 | // voice 2 and 3 cannot set lowest 4 bits of accumulator 42 | uint8_t sample_no = address - 0x6 + 1; 43 | w->voices[1].accumulator &= ~(0b1111 << (sample_no * 4)); 44 | w->voices[1].accumulator |= (value & 0b1111) << (sample_no * 4); 45 | } 46 | 47 | // waveform 3 48 | else if (address == 0xf) { 49 | w->voices[2].waveform_no = value & 0b111; 50 | } else if (address >= 0x1b && address <= 0x1e) { 51 | // voice 2 and 3 cannot set lowest 4 bits of frequency 52 | uint8_t sample_no = address - 0x1b + 1; 53 | w->voices[2].frequency &= ~(0b1111 << (sample_no * 4)); 54 | w->voices[2].frequency |= (value & 0b1111) << (sample_no * 4); 55 | } else if (address == 0x1f) { 56 | w->voices[2].volume = value & 0xf; 57 | } else if (address >= 0xb && address <= 0xe) { 58 | // voice 2 and 3 cannot set lowest 4 bits of accumulator 59 | uint8_t sample_no = address - 0xb + 1; 60 | w->voices[2].accumulator &= ~(0b1111 << (sample_no * 4)); 61 | w->voices[2].accumulator |= (value & 0b1111) << (sample_no * 4); 62 | } 63 | } 64 | 65 | void wsg_play(wsg* const w, int16_t* const buffer, int buffer_len) { 66 | for (int i = 0; i < buffer_len; i++) { 67 | int16_t sample = 0; 68 | 69 | for (int voice_no = 0; voice_no < 3; voice_no++) { 70 | wsg_voice* const v = &w->voices[voice_no]; 71 | 72 | if (v->frequency == 0 || v->volume == 0) { 73 | continue; 74 | } 75 | 76 | v->accumulator = (v->accumulator + v->frequency) & 0xfffff; 77 | 78 | // we use the highest 5 bits of the accumulator (which is 79 | // a 20 bit value) to select a sample (0-31) from a 32-step 80 | // waveform (1 step = 1 byte) in sound rom 81 | int sample_pos = v->waveform_no * 32 + (v->accumulator >> 15); 82 | 83 | // convert unsigned 8 bit sample to a signed 16 bit sample, 84 | // and multiply it by the volume of the voice 85 | int16_t voice_sample = (w->sound_rom[sample_pos] - 8) * v->volume; 86 | sample += voice_sample; 87 | } 88 | 89 | *(buffer + i) = sample * w->gain; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #ifdef __EMSCRIPTEN__ 10 | #include 11 | #endif 12 | 13 | #include "pac.h" 14 | 15 | #define CONTROLLER_DEADZONE 8000 16 | 17 | static bool should_quit = false; 18 | static bool has_focus = true; 19 | static bool is_paused = false; 20 | static int speed = 1; 21 | 22 | static SDL_Renderer* renderer = NULL; 23 | static SDL_Texture* texture = NULL; 24 | static SDL_GameController* controller = NULL; 25 | static SDL_AudioDeviceID audio_device = 0; 26 | 27 | static pac* p = NULL; 28 | static uint32_t current_time = 0; 29 | static uint32_t last_time = 0; 30 | static uint32_t dt = 0; 31 | 32 | static void update_screen(pac* const p) { 33 | int pitch = 0; 34 | void* pixels = NULL; 35 | if (SDL_LockTexture(texture, NULL, &pixels, &pitch) != 0) { 36 | SDL_Log("Unable to lock texture: %s", SDL_GetError()); 37 | } else { 38 | SDL_memcpy(pixels, p->screen_buffer, pitch * PAC_SCREEN_HEIGHT); 39 | } 40 | SDL_UnlockTexture(texture); 41 | } 42 | 43 | static void push_sample(pac* const p, int16_t sample) { 44 | SDL_QueueAudio(audio_device, &sample, sizeof(int16_t) * 1); 45 | } 46 | 47 | static void send_quit_event() { 48 | should_quit = true; 49 | } 50 | 51 | static void screenshot(pac* const p) { 52 | // generate filename 53 | time_t t = time(NULL); 54 | struct tm tm = *localtime(&t); 55 | 56 | char* filename = SDL_calloc(50, sizeof(char)); 57 | 58 | sprintf(filename, "%d-%d-%d %d.%d.%d - %s.bmp", tm.tm_year + 1900, 59 | tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, "pac"); 60 | 61 | // if file already exists, we don't want to erase it 62 | FILE* f = fopen(filename, "r"); 63 | if (f != NULL) { 64 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, 65 | "Cannot save screenshot: file %s already exists", filename); 66 | SDL_free(filename); 67 | fclose(f); 68 | return; 69 | } 70 | fclose(f); 71 | 72 | // render screen buffer to BMP file 73 | const uint32_t pitch = sizeof(uint8_t) * 3 * PAC_SCREEN_WIDTH; 74 | const uint8_t depth = 32; 75 | SDL_Surface* s = SDL_CreateRGBSurfaceWithFormatFrom(p->screen_buffer, 76 | PAC_SCREEN_WIDTH, PAC_SCREEN_HEIGHT, depth, pitch, SDL_PIXELFORMAT_RGB24); 77 | SDL_SaveBMP(s, filename); 78 | SDL_FreeSurface(s); 79 | 80 | SDL_Log("Saved screenshot: %s", filename); 81 | SDL_free(filename); 82 | } 83 | 84 | static void mainloop(void) { 85 | current_time = SDL_GetTicks(); 86 | dt = current_time - last_time; 87 | 88 | SDL_Event e; 89 | while (SDL_PollEvent(&e) != 0) { 90 | if (e.type == SDL_QUIT) { 91 | should_quit = true; 92 | } else if (e.type == SDL_WINDOWEVENT) { 93 | if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { 94 | has_focus = true; 95 | } else if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { 96 | has_focus = false; 97 | } 98 | } else if (e.type == SDL_KEYDOWN) { 99 | switch (e.key.keysym.scancode) { 100 | case SDL_SCANCODE_RETURN: 101 | case SDL_SCANCODE_1: p->p1_start = 1; break; // start (1p) 102 | case SDL_SCANCODE_2: p->p2_start = 1; break; // start (2p) 103 | case SDL_SCANCODE_UP: p->p1_up = 1; break; // up 104 | case SDL_SCANCODE_DOWN: p->p1_down = 1; break; // down 105 | case SDL_SCANCODE_LEFT: p->p1_left = 1; break; // left 106 | case SDL_SCANCODE_RIGHT: p->p1_right = 1; break; // right 107 | case SDL_SCANCODE_C: 108 | case SDL_SCANCODE_5: p->coin_s1 = 1; break; // coin 109 | case SDL_SCANCODE_V: p->coin_s2 = 1; break; // coin (slot 2) 110 | case SDL_SCANCODE_T: p->board_test = 1; break; // board test 111 | 112 | case SDL_SCANCODE_M: p->mute_audio = !p->mute_audio; break; 113 | case SDL_SCANCODE_P: is_paused = !is_paused; break; 114 | case SDL_SCANCODE_S: screenshot(p); break; 115 | case SDL_SCANCODE_I: pac_cheat_invincibility(p); break; 116 | case SDL_SCANCODE_TAB: speed = 5; break; 117 | default: break; 118 | } 119 | } else if (e.type == SDL_KEYUP) { 120 | switch (e.key.keysym.scancode) { 121 | case SDL_SCANCODE_RETURN: 122 | case SDL_SCANCODE_1: p->p1_start = 0; break; // start (1p) 123 | case SDL_SCANCODE_2: p->p2_start = 0; break; // start (2p) 124 | case SDL_SCANCODE_UP: p->p1_up = 0; break; // up 125 | case SDL_SCANCODE_DOWN: p->p1_down = 0; break; // down 126 | case SDL_SCANCODE_LEFT: p->p1_left = 0; break; // left 127 | case SDL_SCANCODE_RIGHT: p->p1_right = 0; break; // right 128 | case SDL_SCANCODE_C: 129 | case SDL_SCANCODE_5: p->coin_s1 = 0; break; // coin 130 | case SDL_SCANCODE_V: p->coin_s2 = 0; break; // coin (slot 2) 131 | case SDL_SCANCODE_T: p->board_test = 0; break; // board test 132 | case SDL_SCANCODE_TAB: 133 | speed = 1; 134 | // clear the queued audio to avoid audio delays 135 | SDL_ClearQueuedAudio(audio_device); 136 | break; 137 | default: break; 138 | } 139 | } else if (e.type == SDL_CONTROLLERBUTTONDOWN) { 140 | switch (e.cbutton.button) { 141 | case SDL_CONTROLLER_BUTTON_A: p->coin_s1 = 1; break; 142 | 143 | case SDL_CONTROLLER_BUTTON_DPAD_UP: p->p1_up = 1; break; 144 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: p->p1_down = 1; break; 145 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: p->p1_left = 1; break; 146 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: p->p1_right = 1; break; 147 | 148 | case SDL_CONTROLLER_BUTTON_START: p->p1_start = 1; break; 149 | case SDL_CONTROLLER_BUTTON_BACK: p->p2_start = 1; break; 150 | } 151 | } else if (e.type == SDL_CONTROLLERBUTTONUP) { 152 | switch (e.cbutton.button) { 153 | case SDL_CONTROLLER_BUTTON_A: p->coin_s1 = 0; break; 154 | 155 | case SDL_CONTROLLER_BUTTON_DPAD_UP: p->p1_up = 0; break; 156 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: p->p1_down = 0; break; 157 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: p->p1_left = 0; break; 158 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: p->p1_right = 0; break; 159 | 160 | case SDL_CONTROLLER_BUTTON_START: p->p1_start = 0; break; 161 | case SDL_CONTROLLER_BUTTON_BACK: p->p2_start = 0; break; 162 | } 163 | } else if (e.type == SDL_CONTROLLERAXISMOTION) { 164 | switch (e.caxis.axis) { 165 | case SDL_CONTROLLER_AXIS_LEFTX: 166 | if (e.caxis.value < -CONTROLLER_DEADZONE) { 167 | p->p1_left = 1; 168 | } else if (e.caxis.value > CONTROLLER_DEADZONE) { 169 | p->p1_right = 1; 170 | } else { 171 | p->p1_left = 0; 172 | p->p1_right = 0; 173 | } 174 | break; 175 | case SDL_CONTROLLER_AXIS_LEFTY: 176 | if (e.caxis.value < -CONTROLLER_DEADZONE) { 177 | p->p1_up = 1; 178 | } else if (e.caxis.value > CONTROLLER_DEADZONE) { 179 | p->p1_down = 1; 180 | } else { 181 | p->p1_up = 0; 182 | p->p1_down = 0; 183 | } 184 | break; 185 | } 186 | } else if (e.type == SDL_CONTROLLERDEVICEADDED) { 187 | const int controller_id = e.cdevice.which; 188 | if (controller == NULL && SDL_IsGameController(controller_id)) { 189 | controller = SDL_GameControllerOpen(controller_id); 190 | } 191 | } else if (e.type == SDL_CONTROLLERDEVICEREMOVED) { 192 | if (controller != NULL) { 193 | SDL_GameControllerClose(controller); 194 | controller = NULL; 195 | } 196 | } 197 | } 198 | 199 | if (!is_paused && has_focus) { 200 | pac_update(p, dt * speed); 201 | } 202 | 203 | SDL_RenderClear(renderer); 204 | SDL_RenderCopy(renderer, texture, NULL, NULL); 205 | SDL_RenderPresent(renderer); 206 | 207 | last_time = current_time; 208 | } 209 | 210 | int main(int argc, char** argv) { 211 | signal(SIGINT, send_quit_event); 212 | 213 | // SDL init 214 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 215 | 0) { 216 | SDL_Log("Unable to initialise SDL: %s", SDL_GetError()); 217 | return 1; 218 | } 219 | 220 | SDL_SetHint(SDL_HINT_BMP_SAVE_LEGACY_FORMAT, "1"); 221 | 222 | // create SDL window 223 | SDL_Window* window = SDL_CreateWindow("pac", SDL_WINDOWPOS_CENTERED, 224 | SDL_WINDOWPOS_CENTERED, PAC_SCREEN_WIDTH * 2, PAC_SCREEN_HEIGHT * 2, 225 | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); 226 | 227 | if (window == NULL) { 228 | SDL_Log("Unable to create window: %s", SDL_GetError()); 229 | return 1; 230 | } 231 | 232 | SDL_SetWindowMinimumSize(window, PAC_SCREEN_WIDTH, PAC_SCREEN_HEIGHT); 233 | 234 | // create renderer 235 | renderer = SDL_CreateRenderer( 236 | window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); 237 | if (renderer == NULL) { 238 | SDL_Log("Unable to create renderer: %s", SDL_GetError()); 239 | return 1; 240 | } 241 | 242 | SDL_RenderSetLogicalSize(renderer, PAC_SCREEN_WIDTH, PAC_SCREEN_HEIGHT); 243 | 244 | // print info on renderer: 245 | SDL_RendererInfo renderer_info; 246 | SDL_GetRendererInfo(renderer, &renderer_info); 247 | SDL_Log("Using renderer %s", renderer_info.name); 248 | 249 | texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB24, 250 | SDL_TEXTUREACCESS_STREAMING, PAC_SCREEN_WIDTH, PAC_SCREEN_HEIGHT); 251 | if (texture == NULL) { 252 | SDL_Log("Unable to create texture: %s", SDL_GetError()); 253 | return 1; 254 | } 255 | 256 | // audio init 257 | SDL_AudioSpec audio_spec; 258 | SDL_zero(audio_spec); 259 | audio_spec.freq = 44100; 260 | audio_spec.format = AUDIO_S16SYS; 261 | audio_spec.channels = 1; 262 | audio_spec.samples = 1024; 263 | audio_spec.callback = NULL; 264 | 265 | audio_device = SDL_OpenAudioDevice(NULL, 0, &audio_spec, NULL, 0); 266 | 267 | if (audio_device == 0) { 268 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "failed to open audio: %s", 269 | SDL_GetError()); 270 | } else { 271 | const char* driver_name = SDL_GetCurrentAudioDriver(); 272 | SDL_Log("audio device has been opened (%s)", driver_name); 273 | } 274 | 275 | SDL_PauseAudioDevice(audio_device, 0); // start playing 276 | 277 | // controller init: opening the first available controller 278 | controller = NULL; 279 | for (int i = 0; i < SDL_NumJoysticks(); i++) { 280 | if (SDL_IsGameController(i)) { 281 | controller = SDL_GameControllerOpen(i); 282 | if (controller) { 283 | SDL_Log( 284 | "game controller detected: %s", SDL_GameControllerNameForIndex(i)); 285 | break; 286 | } else { 287 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, 288 | "could not open game controller: %s", SDL_GetError()); 289 | } 290 | } 291 | } 292 | 293 | // pac init 294 | char* base_path = SDL_GetBasePath(); 295 | // ignoring "-psn" argument from macOS Finder 296 | // https://hg.libsdl.org/SDL/file/c005c49beaa9/test/testdropfile.c#l47 297 | if (argc > 1 && SDL_strncmp(argv[1], "-psn", 4) == 0) { 298 | argc -= 1; 299 | argv += 1; 300 | } 301 | char* rom_dir = argc > 1 ? argv[1] : base_path; 302 | 303 | p = SDL_calloc(1, sizeof(pac)); 304 | if (pac_init(p, rom_dir) != 0) { 305 | SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Missing rom files", 306 | "Please copy rom files next to pac executable.", window); 307 | return 1; 308 | } 309 | p->sample_rate = audio_spec.freq; 310 | p->push_sample = push_sample; 311 | p->update_screen = update_screen; 312 | update_screen(p); 313 | 314 | SDL_free(base_path); 315 | 316 | // main loop 317 | current_time = SDL_GetTicks(); 318 | last_time = SDL_GetTicks(); 319 | #ifdef __EMSCRIPTEN__ 320 | emscripten_set_main_loop(mainloop, 0, 1); 321 | #else 322 | while (!should_quit) { 323 | mainloop(); 324 | } 325 | #endif 326 | 327 | pac_quit(p); 328 | SDL_free(p); 329 | 330 | if (controller != NULL) { 331 | SDL_GameControllerClose(controller); 332 | } 333 | 334 | SDL_DestroyTexture(texture); 335 | SDL_DestroyRenderer(renderer); 336 | SDL_DestroyWindow(window); 337 | SDL_CloseAudioDevice(audio_device); 338 | SDL_Quit(); 339 | 340 | return 0; 341 | } 342 | -------------------------------------------------------------------------------- /src/pac.c: -------------------------------------------------------------------------------- 1 | #include "pac.h" 2 | 3 | static uint8_t rb(void* userdata, uint16_t addr) { 4 | pac* const p = (pac*) userdata; 5 | 6 | // according to https://www.csh.rit.edu/~jerry/arcade/pacman/daves/ 7 | // the highest bit of the address is unused 8 | addr &= 0x7fff; 9 | 10 | if (addr < 0x4000) { 11 | return p->rom[addr]; 12 | } else if (addr < 0x5000) { 13 | return p->ram[addr - 0x4000]; 14 | } else if (addr <= 0x50ff) { // io 15 | if (addr == 0x5003) { 16 | return p->flip_screen; 17 | } else if (addr == 0x5004 || addr == 0x5005) { 18 | // lamps, not used in pacman 19 | return 0; 20 | } else if (addr == 0x5006) { 21 | // coin lockout, not used in pacman 22 | return 0; 23 | } else if (addr == 0x5007) { 24 | // coin counter 25 | } else if (addr >= 0x5000 && addr <= 0x503f) { // in 0 26 | return (!p->p1_up << 0) | (!p->p1_left << 1) | (!p->p1_right << 2) | 27 | (!p->p1_down << 3) | (!p->rack_advance << 4) | (!p->coin_s1 << 5) | 28 | (!p->coin_s2 << 6) | (!p->credits_btn << 7); 29 | } else if (addr >= 0x5040 && addr <= 0x507f) { // in 1 30 | return (!p->p1_up << 0) | (!p->p1_left << 1) | (!p->p1_right << 2) | 31 | (!p->p1_down << 3) | (!p->board_test << 4) | (!p->p1_start << 5) | 32 | (!p->p2_start << 6) | (1 << 7); // cabinet mode: 1=upright 0=table 33 | } else if (addr >= 0x5080 && addr <= 0x50bf) { // dip switch 34 | // bit 0-1: 1 Coin 1 Credit 35 | // bit 2-3: 3 Pacman Per Game 36 | // bit 4-5: Bonus Player @ 10000 Pts 37 | // bit 6: Difficulty (normal=1, hard=0) 38 | // bit 7: Alternate ghost names 39 | return 0b11001001; 40 | } 41 | } else { 42 | printf("ERR: read at %04x\n", addr); 43 | return 0; 44 | } 45 | 46 | return 0xff; 47 | } 48 | 49 | static void wb(void* userdata, uint16_t addr, uint8_t val) { 50 | pac* const p = (pac*) userdata; 51 | 52 | // according to https://www.csh.rit.edu/~jerry/arcade/pacman/daves/ 53 | // the highest bit of the address is unused 54 | addr &= 0x7fff; 55 | 56 | if (addr < 0x4000) { 57 | // cannot write to rom 58 | } else if (addr < 0x5000) { 59 | p->ram[addr - 0x4000] = val; 60 | } else if (addr <= 0x50ff) { // io 61 | if (addr == 0x5000) { 62 | p->vblank_enabled = val & 1; 63 | } else if (addr == 0x5001) { 64 | p->sound_enabled = val & 1; 65 | } else if (addr == 0x5002) { 66 | // aux board? 67 | } else if (addr == 0x5003) { 68 | p->flip_screen = val & 1; 69 | } else if (addr == 0x5004 || addr == 0x5005) { 70 | // lamps, not used in pacman 71 | } else if (addr == 0x5006) { 72 | // coin lockout, not used in pacman 73 | } else if (addr == 0x5007) { 74 | // coin counter 75 | } else if (addr >= 0x5040 && addr <= 0x505f) { // audio 76 | wsg_write(&p->sound_chip, addr - 0x5040, val); 77 | } else if (addr >= 0x5060 && addr <= 0x506f) { 78 | p->sprite_pos[addr - 0x5060] = val; 79 | } else if (addr >= 0x50c0 && addr <= 0x50ff) { 80 | // watchdog: no action is needed here, because watchdog is not 81 | // implemented on the emu. 82 | } 83 | } else { 84 | printf("ERR: write %02x at %04x\n", val, addr); 85 | } 86 | } 87 | 88 | static uint8_t port_in(z80* const z, uint8_t port) { 89 | return 0; 90 | } 91 | 92 | static void port_out(z80* const z, uint8_t port, uint8_t val) { 93 | pac* const p = (pac*) z->userdata; 94 | 95 | // setting the interrupt vector 96 | if (port == 0) { 97 | p->int_vector = val; 98 | } 99 | } 100 | 101 | // appends two NULL-terminated strings, creating a new string in the process. 102 | // The pointer returned is owned by the user. 103 | static inline char* append_path(const char* s1, const char* s2) { 104 | const int buf_size = strlen(s1) + strlen(s2) + 1; 105 | char* path = calloc(buf_size, sizeof(char)); 106 | if (path == NULL) { 107 | return NULL; 108 | } 109 | snprintf(path, buf_size, "%s%s", s1, s2); 110 | return path; 111 | } 112 | 113 | // copies "nb_bytes" bytes from a file into memory 114 | static inline int load_file( 115 | const char* filename, uint8_t* memory, size_t nb_bytes) { 116 | FILE* f = fopen(filename, "rb"); 117 | if (f == NULL) { 118 | fprintf(stderr, "error: can't open file '%s'.\n", filename); 119 | return 1; 120 | } 121 | 122 | size_t result = fread(memory, sizeof(uint8_t), nb_bytes, f); 123 | if (result != nb_bytes) { 124 | fprintf(stderr, "error: while reading file '%s'\n", filename); 125 | return 1; 126 | } 127 | 128 | fclose(f); 129 | return 0; 130 | } 131 | 132 | // MARK: graphics 133 | 134 | // the color palette is stored in color_rom (82s123.7f). Each byte corresponds 135 | // to one color and is composed of three components red, green and blue 136 | // following that pattern: 0bBBGGGRRR. 137 | // Each color component corresponds to a color intensity. 138 | // @TODO: add comment on how to get from color intensity to RGB color. 139 | static inline void get_color( 140 | pac* const p, uint8_t color_no, uint8_t* r, uint8_t* g, uint8_t* b) { 141 | const uint8_t data = p->color_rom[color_no]; 142 | *r = ((data >> 0) & 1) * 0x21 + ((data >> 1) & 1) * 0x47 + 143 | ((data >> 2) & 1) * 0x97; 144 | *g = ((data >> 3) & 1) * 0x21 + ((data >> 4) & 1) * 0x47 + 145 | ((data >> 5) & 1) * 0x97; 146 | *b = ((data >> 6) & 1) * 0x51 + ((data >> 7) & 1) * 0xae; 147 | } 148 | 149 | // Color palettes are defined in palette_rom (82s126.4a): each palette contains 150 | // four colors (one byte for each color). 151 | static inline void get_palette(pac* const p, uint8_t pal_no, uint8_t* pal) { 152 | pal_no &= 0x3f; 153 | pal[0] = p->palette_rom[pal_no * 4 + 0]; 154 | pal[1] = p->palette_rom[pal_no * 4 + 1]; 155 | pal[2] = p->palette_rom[pal_no * 4 + 2]; 156 | pal[3] = p->palette_rom[pal_no * 4 + 3]; 157 | } 158 | 159 | // decodes a strip from pacman tile/sprite roms to a bitmap output where each 160 | // byte represents one pixel. 161 | static inline void decode_strip(pac* const p, uint8_t* const input, 162 | uint8_t* const output, int bx, int by, int img_width) { 163 | const int base_i = by * img_width + bx; 164 | for (int x = 0; x < 8; x++) { 165 | uint8_t strip = *(input + x); 166 | 167 | for (int y = 0; y < 4; y++) { 168 | // bitmaps are stored mirrored in memory, so we need to read it 169 | // starting from the bottom right: 170 | int i = (3 - y) * img_width + 7 - x; 171 | output[base_i + i] = (strip >> (y % 4)) & 1; 172 | output[base_i + i] |= ((strip >> (y % 4 + 4)) & 1) << 1; 173 | } 174 | } 175 | } 176 | 177 | // preloads sprites and tiles 178 | static inline void preload_images(pac* const p) { 179 | // sprites and tiles are images that are stored in sprite/tile rom. 180 | // in memory, those images are represented using vertical "strips" 181 | // of 8*4px, each strip being 8 bytes long (each pixel is stored on two 182 | // bits) 183 | const int LEN_STRIP_BYTES = 8; 184 | 185 | // tiles are 8*8px images. in memory, they are composed of two strips. 186 | const int NB_PIXELS_PER_TILE = 8 * 8; 187 | const int TILE_WIDTH = 8; 188 | const int NB_TILES = 256; 189 | memset(p->tiles, 0, NB_TILES * NB_PIXELS_PER_TILE); 190 | for (int i = 0; i < NB_TILES; i++) { 191 | uint8_t* const tile = &p->tiles[i * NB_PIXELS_PER_TILE]; 192 | uint8_t* const rom = &p->tile_rom[i * (LEN_STRIP_BYTES * 2)]; 193 | 194 | decode_strip(p, rom + 0, tile, 0, 4, TILE_WIDTH); 195 | decode_strip(p, rom + 8, tile, 0, 0, TILE_WIDTH); 196 | } 197 | 198 | // sprites are 16*16px images. in memory, they are composed of 8 strips. 199 | const int NB_PIXELS_PER_SPRITE = 16 * 16; 200 | const int SPRITE_WIDTH = 16; 201 | const int NB_SPRITES = 64; 202 | memset(p->sprites, 0, NB_SPRITES * NB_PIXELS_PER_SPRITE); 203 | for (int i = 0; i < NB_SPRITES; i++) { 204 | uint8_t* const sprite = &p->sprites[i * NB_PIXELS_PER_SPRITE]; 205 | uint8_t* const rom = &p->sprite_rom[i * (LEN_STRIP_BYTES * 8)]; 206 | 207 | decode_strip(p, rom + 0 * 8, sprite, 8, 12, SPRITE_WIDTH); 208 | decode_strip(p, rom + 1 * 8, sprite, 8, 0, SPRITE_WIDTH); 209 | decode_strip(p, rom + 2 * 8, sprite, 8, 4, SPRITE_WIDTH); 210 | decode_strip(p, rom + 3 * 8, sprite, 8, 8, SPRITE_WIDTH); 211 | 212 | decode_strip(p, rom + 4 * 8, sprite, 0, 12, SPRITE_WIDTH); 213 | decode_strip(p, rom + 5 * 8, sprite, 0, 0, SPRITE_WIDTH); 214 | decode_strip(p, rom + 6 * 8, sprite, 0, 4, SPRITE_WIDTH); 215 | decode_strip(p, rom + 7 * 8, sprite, 0, 8, SPRITE_WIDTH); 216 | } 217 | } 218 | 219 | static inline void draw_tile( 220 | pac* const p, uint8_t tile_no, uint8_t* pal, uint16_t x, uint16_t y) { 221 | if (x < 0 || x >= PAC_SCREEN_WIDTH) { 222 | return; 223 | } 224 | 225 | for (int i = 0; i < 8 * 8; i++) { 226 | int px = i % 8; 227 | int py = i / 8; 228 | 229 | uint8_t color = p->tiles[tile_no * 64 + i]; 230 | int screenbuf_pos = (y + py) * PAC_SCREEN_WIDTH + (x + px); 231 | 232 | get_color(p, pal[color], &p->screen_buffer[screenbuf_pos * 3 + 0], 233 | &p->screen_buffer[screenbuf_pos * 3 + 1], 234 | &p->screen_buffer[screenbuf_pos * 3 + 2]); 235 | } 236 | } 237 | 238 | static inline void draw_sprite(pac* const p, uint8_t sprite_no, uint8_t* pal, 239 | int16_t x, int16_t y, bool flip_x, bool flip_y) { 240 | if (x <= -16 || x > PAC_SCREEN_WIDTH) { 241 | return; 242 | } 243 | 244 | const int base_i = y * PAC_SCREEN_WIDTH + x; 245 | 246 | for (int i = 0; i < 16 * 16; i++) { 247 | int px = i % 16; 248 | int py = i / 16; 249 | 250 | uint8_t color = p->sprites[sprite_no * 256 + i]; 251 | 252 | // color 0 is transparent 253 | if (pal[color] == 0) { 254 | continue; 255 | } 256 | 257 | int x_pos = flip_x ? 15 - px : px; 258 | int y_pos = flip_y ? 15 - py : py; 259 | int screenbuf_pos = base_i + y_pos * PAC_SCREEN_WIDTH + x_pos; 260 | 261 | if (x + x_pos < 0 || x + x_pos >= PAC_SCREEN_WIDTH) { 262 | continue; 263 | } 264 | 265 | get_color(p, pal[color], &p->screen_buffer[screenbuf_pos * 3 + 0], 266 | &p->screen_buffer[screenbuf_pos * 3 + 1], 267 | &p->screen_buffer[screenbuf_pos * 3 + 2]); 268 | } 269 | } 270 | 271 | static inline void pac_draw(pac* const p) { 272 | // 1. writing tiles according to VRAM 273 | 274 | const uint16_t VRAM_SCREEN_BOT = 0x4000; 275 | const uint16_t VRAM_SCREEN_MID = 0x4000 + 64; 276 | const uint16_t VRAM_SCREEN_TOP = 0x4000 + 64 + 0x380; 277 | 278 | int x, y, i; 279 | uint8_t palette[4]; 280 | 281 | // bottom of screen: 282 | x = 31; 283 | y = 34; 284 | i = VRAM_SCREEN_BOT; 285 | while (x != 31 || y != 36) { 286 | const uint8_t tile_no = rb(p, i); 287 | const uint8_t palette_no = rb(p, i + 0x400); 288 | 289 | get_palette(p, palette_no, palette); 290 | draw_tile(p, tile_no, palette, (x - 2) * 8, y * 8); 291 | 292 | i += 1; 293 | if (x == 0) { 294 | x = 31; 295 | y += 1; 296 | } else { 297 | x -= 1; 298 | } 299 | } 300 | 301 | // middle of the screen: 302 | x = 29; 303 | y = 2; 304 | i = VRAM_SCREEN_MID; 305 | while (x != 1 || y != 2) { 306 | const uint8_t tile_no = rb(p, i); 307 | const uint8_t palette_no = rb(p, i + 0x400); 308 | 309 | get_palette(p, palette_no, palette); 310 | draw_tile(p, tile_no, palette, (x - 2) * 8, y * 8); 311 | 312 | i += 1; 313 | if (y == 33) { 314 | y = 2; 315 | x -= 1; 316 | } else { 317 | y += 1; 318 | } 319 | } 320 | 321 | // top of the screen: 322 | x = 31; 323 | y = 0; 324 | i = VRAM_SCREEN_TOP; 325 | while (x != 31 || y != 2) { 326 | const uint8_t tile_no = rb(p, i); 327 | const uint8_t palette_no = rb(p, i + 0x400); 328 | 329 | get_palette(p, palette_no, palette); 330 | draw_tile(p, tile_no, palette, (x - 2) * 8, y * 8); 331 | 332 | i += 1; 333 | if (x == 0) { 334 | x = 31; 335 | y += 1; 336 | } else { 337 | x -= 1; 338 | } 339 | } 340 | 341 | // 2. drawing the 8 sprites (in reverse order) 342 | const uint16_t VRAM_SPRITES_INFO = 0x4FF0; 343 | for (int s = 7; s >= 0; s--) { 344 | // the screen coordinates of a sprite start on the lower right corner 345 | // of the main screen: 346 | const int16_t x = PAC_SCREEN_WIDTH - p->sprite_pos[s * 2] + 15; 347 | const int16_t y = PAC_SCREEN_HEIGHT - p->sprite_pos[s * 2 + 1] - 16; 348 | 349 | const uint8_t sprite_info = rb(p, VRAM_SPRITES_INFO + s * 2); 350 | const uint8_t palette_no = rb(p, VRAM_SPRITES_INFO + s * 2 + 1); 351 | 352 | const bool flip_x = (sprite_info >> 1) & 1; 353 | const bool flip_y = (sprite_info >> 0) & 1; 354 | const uint8_t sprite_no = sprite_info >> 2; 355 | 356 | get_palette(p, palette_no, palette); 357 | draw_sprite(p, sprite_no, palette, x, y, flip_x, flip_y); 358 | } 359 | } 360 | 361 | // generates audio for one frame 362 | static inline void sound_update(pac* const p) { 363 | if (!p->sound_enabled || p->mute_audio) { 364 | return; 365 | } 366 | 367 | // update the WSG (filling the audio buffer) 368 | wsg_play(&p->sound_chip, p->audio_buffer, p->audio_buffer_len); 369 | 370 | // resampling the 96kHz audio stream from the WSG into a 44.1kHz one 371 | float d = (float) WSG_SAMPLE_RATE / (float) p->sample_rate; 372 | for (int i = 0; i < p->sample_rate / PAC_FPS; i++) { 373 | int pos = d * (float) i; 374 | p->push_sample(p, p->audio_buffer[pos]); 375 | } 376 | } 377 | 378 | int pac_init(pac* const p, const char* rom_dir) { 379 | z80_init(&p->cpu); 380 | p->cpu.userdata = p; 381 | p->cpu.read_byte = rb; 382 | p->cpu.write_byte = wb; 383 | p->cpu.port_in = port_in; 384 | p->cpu.port_out = port_out; 385 | 386 | memset(p->rom, 0, sizeof(p->rom)); 387 | memset(p->ram, 0, sizeof(p->ram)); 388 | memset(p->sprite_pos, 0, sizeof(p->sprite_pos)); 389 | memset(p->screen_buffer, 0, sizeof(p->screen_buffer)); 390 | 391 | p->int_vector = 0; 392 | p->vblank_enabled = 0; 393 | p->sound_enabled = 0; 394 | p->flip_screen = 0; 395 | 396 | // in 0 port 397 | p->p1_up = 0; 398 | p->p1_left = 0; 399 | p->p1_right = 0; 400 | p->p1_down = 0; 401 | p->rack_advance = 0; 402 | p->coin_s1 = 0; 403 | p->coin_s2 = 0; 404 | p->credits_btn = 0; 405 | 406 | // in 1 port 407 | p->board_test = 0; 408 | p->p1_start = 0; 409 | p->p2_start = 0; 410 | 411 | // loading rom files 412 | int r = 0; 413 | char* file0 = append_path(rom_dir, "pacman.6e"); 414 | r += load_file(file0, &p->rom[0x0000], 0x1000); 415 | char* file1 = append_path(rom_dir, "pacman.6f"); 416 | r += load_file(file1, &p->rom[0x1000], 0x1000); 417 | char* file2 = append_path(rom_dir, "pacman.6h"); 418 | r += load_file(file2, &p->rom[0x2000], 0x1000); 419 | char* file3 = append_path(rom_dir, "pacman.6j"); 420 | r += load_file(file3, &p->rom[0x3000], 0x1000); 421 | char* file4 = append_path(rom_dir, "82s123.7f"); 422 | r += load_file(file4, p->color_rom, 32); 423 | char* file5 = append_path(rom_dir, "82s126.4a"); 424 | r += load_file(file5, p->palette_rom, 0x100); 425 | char* file6 = append_path(rom_dir, "pacman.5e"); 426 | r += load_file(file6, p->tile_rom, 0x1000); 427 | char* file7 = append_path(rom_dir, "pacman.5f"); 428 | r += load_file(file7, p->sprite_rom, 0x1000); 429 | char* file8 = append_path(rom_dir, "82s126.1m"); 430 | r += load_file(file8, p->sound_rom1, 0x100); 431 | // char* file9 = append_path(rom_dir, "82s126.3m"); 432 | // r += load_file(file9, p->sound_rom2, 0x100); 433 | 434 | // @TODO: mspacman 435 | if (0) { 436 | char* file6 = append_path(rom_dir, "5e"); 437 | r += load_file(file6, p->tile_rom, 0x1000); 438 | char* file7 = append_path(rom_dir, "5f"); 439 | r += load_file(file7, p->sprite_rom, 0x1000); 440 | 441 | char* file10 = append_path(rom_dir, "u5"); 442 | r += load_file(file10, &p->rom[0x8000], 2048); 443 | char* file11 = append_path(rom_dir, "u6"); 444 | r += load_file(file11, &p->rom[0x9000], 4096); 445 | char* file12 = append_path(rom_dir, "u7"); 446 | r += load_file(file12, &p->rom[0xB000], 4096); 447 | 448 | free(file10); 449 | free(file11); 450 | free(file12); 451 | } 452 | 453 | free(file0); 454 | free(file1); 455 | free(file2); 456 | free(file3); 457 | free(file4); 458 | free(file5); 459 | free(file6); 460 | free(file7); 461 | free(file8); 462 | // free(file9); 463 | 464 | preload_images(p); 465 | p->update_screen = NULL; 466 | 467 | // audio 468 | wsg_init(&p->sound_chip, p->sound_rom1); 469 | p->audio_buffer_len = WSG_SAMPLE_RATE / PAC_FPS; 470 | p->audio_buffer = calloc(p->audio_buffer_len, sizeof(int16_t)); 471 | p->sample_rate = 44100; 472 | p->mute_audio = false; 473 | p->push_sample = NULL; 474 | 475 | return r != 0; 476 | } 477 | 478 | void pac_quit(pac* const p) { 479 | free(p->audio_buffer); 480 | } 481 | 482 | // updates emulation for "ms" milliseconds. 483 | void pac_update(pac* const p, unsigned int ms) { 484 | // machine executes exactly PAC_CLOCK_SPEED cycles every second, 485 | // so we need to execute "ms * PAC_CLOCK_SPEED / 1000" 486 | int count = 0; 487 | while (count < ms * PAC_CLOCK_SPEED / 1000) { 488 | int cyc = p->cpu.cyc; 489 | z80_step(&p->cpu); 490 | int elapsed = p->cpu.cyc - cyc; 491 | count += elapsed; 492 | 493 | if (p->cpu.cyc >= PAC_CYCLES_PER_FRAME) { 494 | p->cpu.cyc -= PAC_CYCLES_PER_FRAME; 495 | 496 | // trigger vblank if enabled: 497 | if (p->vblank_enabled) { 498 | // p->vblank_enabled = 0; 499 | z80_gen_int(&p->cpu, p->int_vector); 500 | 501 | pac_draw(p); 502 | p->update_screen(p); 503 | sound_update(p); 504 | } 505 | } 506 | } 507 | } 508 | 509 | // invincibility patch (from http://cheat.retrogames.com) 510 | void pac_cheat_invincibility(pac* const p) { 511 | p->rom[0x1774 + 3] = 0x32; 512 | p->rom[0x1774 + 2] = 0x3c; 513 | p->rom[0x1774 + 1] = 0xe0; 514 | p->rom[0x1774 + 0] = 0xc3; 515 | 516 | p->rom[0x3cdf + 3] = 0x04; 517 | p->rom[0x3cdf + 2] = 0x20; 518 | p->rom[0x3cdf + 1] = 0xa7; 519 | p->rom[0x3cdf + 0] = 0x00; 520 | 521 | p->rom[0x3ce3 + 3] = 0x17; 522 | p->rom[0x3ce3 + 2] = 0x64; 523 | p->rom[0x3ce3 + 1] = 0xc3; 524 | p->rom[0x3ce3 + 0] = 0xaf; 525 | 526 | p->rom[0x3ce7 + 3] = 0x17; 527 | p->rom[0x3ce7 + 2] = 0x77; 528 | p->rom[0x3ce7 + 1] = 0xc3; 529 | p->rom[0x3ce7 + 0] = 0xaf; 530 | 531 | printf("applied invincibility patch\n"); 532 | } 533 | --------------------------------------------------------------------------------