#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "callback.h" #include "util.h" #include "toml.hpp" namespace fs = std::filesystem; using namespace std::string_literals; using namespace std::string_view_literals; PSP_MODULE_INFO("psp-chip8", 0, 1, 1); PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU); static unsigned int __attribute__((aligned(16))) list[262144]; #define BUF_WIDTH (512) #define SCR_WIDTH (480) #define SCR_HEIGHT (272) struct Vertex { u32 color; float x, y, z; }; struct TexVertex { float u, v; u32 color; float x, y, z; }; static unsigned int static_offset = 0; static unsigned int get_memory_size(unsigned int width, unsigned int height, unsigned int psm) { switch (psm) { case GU_PSM_T4: return (width * height) >> 1; case GU_PSM_T8: return width * height; case GU_PSM_5650: case GU_PSM_5551: case GU_PSM_4444: case GU_PSM_T16: return 2 * width * height; case GU_PSM_8888: case GU_PSM_T32: return 4 * width * height; default: return 0; } } void *get_static_vram_buffer(unsigned int width, unsigned int height, unsigned int psm) { unsigned int mem_sz = get_memory_size(width, height, psm); void *result = (void *)static_offset; static_offset += mem_sz; return result; } void *get_static_vram_texture(unsigned int width, unsigned int height, unsigned int psm) { void *result = get_static_vram_buffer(width, height, psm); return (void *)(((unsigned int)result) + ((unsigned int)sceGeEdramGetAddr())); } void render_texture(float x, float y, float w, float h, u32 buffer[], int tw, int th) { sceGuTexMode(GU_PSM_8888, 0, 0, 0); sceGuTexImage(0, tw, th, tw, buffer); sceGuTexFunc(GU_TFX_REPLACE, GU_TCC_RGBA); sceGuTexFilter(GU_NEAREST, GU_NEAREST); TexVertex *vertices = (TexVertex *)sceGuGetMemory(2 * sizeof(TexVertex)); vertices[0] = { 0, 0, 0, x, y, 0.0f }; vertices[1] = { 1, 1, 0, x + w, y + h, 0.0f }; sceGumDrawArray(GU_SPRITES, GU_TEXTURE_32BITF | GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, 2, 0, vertices); } void render_rectangle(float x, float y, float w, float h, u32 color) { ScePspFVector3 pos = { x, y, 0.0f }; sceGumPushMatrix(); sceGumTranslate(&pos); Vertex __attribute__((aligned(16))) vertices[1 * 6] = { { color, 0.0f, 0.0f, 0.0f }, // TL { color, 0.0f, h, 0.0f }, // BL { color, w, 0.0f, 0.0f }, // TR { color, w, 0.0f, 0.0f }, // TR { color, 0.0f, h, 0.0f }, // BL { color, w, h, 0.0f }, // BR }; sceGumDrawArray( GU_TRIANGLES, GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, 1 * 6, 0, vertices); sceGumPopMatrix(); } constexpr int const CHIP_W = 64; constexpr int const CHIP_H = 32; enum Key { KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, }; struct KeyMap { std::string name; int keys_normal[4 * 4] = { 0 }; int keys_shift[4 * 4] = { 0 }; }; struct Config { Config(toml::table const &table); std::vector mappings; }; std::unordered_map key_map = { { "Cross", PSP_CTRL_CROSS }, { "Triangle", PSP_CTRL_TRIANGLE }, { "Circle", PSP_CTRL_CIRCLE }, { "Square", PSP_CTRL_SQUARE }, { "Up", PSP_CTRL_UP }, { "Down", PSP_CTRL_DOWN }, { "Left", PSP_CTRL_LEFT }, { "Right", PSP_CTRL_RIGHT }, { "Select", PSP_CTRL_SELECT }, }; std::unordered_map indexes = { { "KEY_1"sv, KEY_1 }, { "KEY_2"sv, KEY_2 }, { "KEY_3"sv, KEY_3 }, { "KEY_C"sv, KEY_C }, { "KEY_4"sv, KEY_4 }, { "KEY_5"sv, KEY_5 }, { "KEY_6"sv, KEY_6 }, { "KEY_D"sv, KEY_D }, { "KEY_7"sv, KEY_7 }, { "KEY_8"sv, KEY_8 }, { "KEY_9"sv, KEY_9 }, { "KEY_E"sv, KEY_E }, { "KEY_A"sv, KEY_A }, { "KEY_0"sv, KEY_0 }, { "KEY_B"sv, KEY_B }, { "KEY_F"sv, KEY_F }, }; void parse_keys(toml::table const &table, int *keys) { for (auto const &[key, value] : table) { if (value.is_string()) { std::string value_str = *value.value(); auto it = key_map.find(value_str); if (it != key_map.end()) { auto it_key = indexes.find(key.str()); if (it_key != indexes.end()) keys[it_key->second] = it->second; } } } } Config::Config(toml::table const &table) { if (auto keymaps = table["keymap"].as_array()) { int i = 1; for (auto const &el : *keymaps) { if (!el.is_table()) { std::cerr << "Error: Element is not a table" << std::endl; continue; } auto const &map_table = *el.as_table(); KeyMap map; if (auto name = map_table["name"].value()) map.name = *name; else map.name = std::string("Unnamed") + std::to_string(i++); if (auto normal = map_table["normal"].as_table()) parse_keys(*normal, map.keys_normal); if (auto shifted = map_table["shifted"].as_table()) parse_keys(*shifted, map.keys_shift); mappings.push_back(map); } } } class Keypad { public: std::vector mappings; void update(void); bool keys[4 * 4] = { 0 }; bool start() const { return m_start; } bool second() const { return m_second; } std::string const &map_name() const { return mappings[m_map].name; } private: SceCtrlData m_pad; bool m_start = false; bool m_second = false; size_t m_map = 0; bool m_prev_rtrigger = false; }; void Keypad::update(void) { sceCtrlReadBufferPositive(&m_pad, 1); for (auto &key : keys) key = false; if (!m_prev_rtrigger && (m_pad.Buttons & PSP_CTRL_RTRIGGER)) m_map = (m_map + 1) % mappings.size(); if (!m_pad.Buttons || mappings.empty()) goto end; m_second = m_pad.Buttons & PSP_CTRL_LTRIGGER; for (int i = 0; i < 4 * 4; i++) { if (m_second) keys[i] = m_pad.Buttons & mappings[m_map].keys_shift[i]; else keys[i] = m_pad.Buttons & mappings[m_map].keys_normal[i]; } end: m_prev_rtrigger = m_pad.Buttons & PSP_CTRL_RTRIGGER; m_start = m_pad.Buttons & PSP_CTRL_START; } struct CHIP8 { byte memory[0x1000]; std::vector stack; byte display[CHIP_W * CHIP_H] = { 0 }; byte registers[16] = { 0 }; Keypad keypad; std::string message; u16 pc = 0x200; u16 i = 0; u8 delay_timer = 0, sound_timer = 0; bool halted = true; CHIP8(); void init(); void load(byte buffer[], int size); void step(); }; u32 *g_fb = nullptr; void CHIP8::load(byte buffer[], int size) { init(); for (int i = 0; i < size; i++) this->memory[0x200 + i] = buffer[i]; this->halted = false; } void update_fb(CHIP8 *chip) { if (!g_fb) return; for (int y = 0; y < CHIP_H; y++) { for (int x = 0; x < CHIP_W; x++) { u32 color = 0xff020202; if (chip->display[x + y * CHIP_W]) color = 0xffaaaaaa; g_fb[x + y * CHIP_W] = color; } } sceKernelDcacheWritebackInvalidateRange(g_fb, CHIP_W * CHIP_H * 4); } void CHIP8::step() { if (halted) return; byte instr[2] { this->memory[this->pc++], this->memory[this->pc++], }; u16 whole = instr[0] << 8 | instr[1]; byte nibbles[4] { static_cast((instr[0] & 0xf0) >> 4), static_cast((instr[0] & 0x0f)), static_cast((instr[1] & 0xf0) >> 4), static_cast((instr[1] & 0x0f)), }; auto X = (whole & 0x0F00) >> 8; auto Y = (whole & 0x00F0) >> 4; auto N = nibbles[3]; auto NN = instr[1]; u16 NNN = (nibbles[1] << 8) | (nibbles[2] << 4) | (nibbles[3]); switch (nibbles[0]) { case 0x0: if (instr[1] == 0xE0) { // CLS - Clear the display. for (auto &i : display) i = false; update_fb(this); } else if (instr[1] == 0xEE) { // RET - Return from a subroutine. this->pc = stack.back(); stack.pop_back(); } else { // Ignore. } break; case 0x1: // JP addr - Jump to location nnn. this->pc = NNN; break; case 0x2: // CALL addr - Call subroutine at nnn. stack.push_back(this->pc); this->pc = NNN; break; case 0x3: // SE Vx, byte - Skip next instruction if Vx = kk. if (this->registers[X] == NN) this->pc += 2; break; case 0x4: // SNE Vx, byte - Skip next instruction if Vx != kk. if (this->registers[X] != NN) this->pc += 2; break; case 0x5: if (nibbles[3] == 0) { // SE Vx, Vy - Skip next instruction if Vx = Vy. if (this->registers[X] == this->registers[Y]) this->pc += 2; } else { this->halted = true; this->pc -= 2; this->message = std::format("Invalid instruction: 0x{:x}", whole); } break; case 0x6: // LD Vx, byte - Set Vx = kk. this->registers[X] = NN; break; case 0x7: // ADD Vx, byte - Set Vx = Vx + kk. this->registers[X] += NN; break; case 0x8: switch (nibbles[3]) { case 0x0: // LD Vx, Vy - Set Vx = Vy. this->registers[X] = this->registers[Y]; break; case 0x1: // OR Vx, Vy - Set Vx = Vx OR Vy. this->registers[X] |= this->registers[Y]; this->registers[0xF] = 0; break; case 0x2: // AND Vx, Vy Set Vx = Vx AND Vy. this->registers[X] &= this->registers[Y]; this->registers[0xF] = 0; break; case 0x3: // XOR Vx, Vy Set Vx = Vx XOR Vy. this->registers[X] ^= this->registers[Y]; this->registers[0xF] = 0; break; case 0x4: { // ADD Vx, Vy Set Vx = Vx + Vy, set VF = carry. unsigned sum = (unsigned)(this->registers[X]) + (unsigned)(this->registers[Y]); this->registers[X] = sum & 0xff; this->registers[0xF] = 0; if (sum > 0xFF) this->registers[0xF] = 1; break; } case 0x5: { // SUB Vx, Vy Set Vx = Vx - Vy, set VF = NOT borrow. bool not_borrow = this->registers[X] > this->registers[Y] ? 1 : 0; this->registers[X] -= this->registers[Y]; this->registers[0xF] = not_borrow; break; } case 0x6: { // SHR Vx {, Vy} Set Vx = Vx SHR 1. bool not_borrow = this->registers[X] & 1; this->registers[X] = this->registers[Y] >> 1; this->registers[0xf] = not_borrow; break; } case 0x7: { // SUBN Vx, Vy Set Vx = Vy - Vx, set VF = NOT borrow. bool not_borrow = this->registers[Y] > this->registers[X] ? 1 : 0; this->registers[X] = this->registers[Y] - this->registers[X]; this->registers[0xF] = not_borrow; break; } case 0xE: // SHL Vx {, Vy} Set Vx = Vx SHL 1. uint8_t vf = this->registers[X] >> 7; this->registers[X] = this->registers[Y] << 1; this->registers[0xF] = vf; break; } break; case 0x9: if (nibbles[3] == 0) { // SNE Vx, Vy - Skip next instruction if Vx != Vy. if (this->registers[X] != this->registers[Y]) this->pc += 2; } else { this->halted = true; this->pc -= 2; this->message = std::format("Invalid instruction: 0x{:x}", whole); } break; case 0xA: // LD I, addr - Set I = nnn. this->i = NNN; break; case 0xB: // JP V0, addr - Jump to location nnn + V0. this->pc = this->registers[0] + NNN; break; case 0xC: // RND Vx, byte - Set Vx = random byte AND kk. this->registers[X] = (rand() % 0x100) & NN; break; case 0xD: { // DRW Vx, Vy, nibble - Display n-byte sprite starting at memory location I at // (Vx, Vy), set VF = collision. byte sx = this->registers[X] % CHIP_W; byte sy = this->registers[Y] % CHIP_H; this->registers[0xF] = 0; for (int y = 0; y < N; y++) { if (sy + y >= CHIP_H) break; byte row = this->memory[this->i + y]; for (int x = 0; x < 8; x++) { if (sx + x >= CHIP_W) break; if ((row & (0x80 >> x)) == 0) continue; if (this->display[(sx + x) % CHIP_W + ((sy + y) % CHIP_H) * CHIP_W]) this->registers[0xF] = 1; this->display[(sx + x) % CHIP_W + ((sy + y) % CHIP_H) * CHIP_W] ^= 1; } } update_fb(this); // Ensure this function is correctly updating the frame buffer break; } case 0xE: if (instr[1] == 0x9e) { // SKP Vx - Skip next instruction if key with the value of Vx is pressed. if (this->keypad.keys[this->registers[X]]) this->pc += 2; } else if (instr[1] == 0xa1) { // SKNP Vx - Skip next instruction if key with the value of Vx is not pressed. if (!this->keypad.keys[this->registers[X]]) this->pc += 2; } else { this->halted = true; this->pc -= 2; this->message = std::format("Invalid instruction: 0x{:x}", whole); } break; case 0xF: switch (instr[1]) { case 0x07: // LD Vx, DT - Set Vx = delay timer value. this->registers[X] = this->delay_timer; break; case 0x0a: { // LD Vx, K - Wait for a key press, store the value of the key in Vx. int idx = 0; bool found = false; for (auto key : this->keypad.keys) { if (key) { found = true; break; } idx++; } if (!found) { this->pc -= 2; this->message = "Awaiting input..."; } else { this->registers[X] = idx; this->message = ""; } break; } case 0x15: // LD DT, Vx - Set delay timer = Vx. this->delay_timer = this->registers[X]; break; case 0x18: // LD ST, Vx - Set sound timer = Vx. this->sound_timer = this->registers[X]; break; case 0x1e: // ADD I, Vx - Set I = I + Vx. this->i += this->registers[X]; break; case 0x29: // LD F, Vx - Set I = location of sprite for digit Vx. this->i = 0x50 + this->registers[X] * 5; break; case 0x33: { // LD B, Vx - Store BCD representation of Vx in memory locations I, I+1, and I+2. byte digits[] { static_cast(this->registers[X] / 100), static_cast((this->registers[X] / 10) % 10), static_cast(this->registers[X] % 10), }; for (int i = 0; i < 3; i++) this->memory[this->i + i] = digits[i]; break; } case 0x55: // LD [I], Vx - Store registers V0 through Vx in memory starting at location I. for (int i = 0; i <= X; i++) this->memory[this->i++] = this->registers[i]; break; case 0x65: // LD Vx, [I] - Read registers V0 through Vx from memory starting at location I. for (int i = 0; i <= X; i++) this->registers[i] = this->memory[this->i++]; break; } break; default: this->halted = true; this->pc -= 2; this->message = std::format("Unknown instruction: {}", nibbles[0]); } } CHIP8::CHIP8() { init(); } void update_fb(CHIP8 *chip); void CHIP8::init() { for (auto &b : this->memory) b = 0; for (auto &b : this->registers) b = 0; for (auto &b : this->display) b = 0; update_fb(this); this->stack.clear(); this->pc = 0x200; this->i = 0; this->delay_timer = this->sound_timer = 0; byte charset[] = { 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 0x20, 0x60, 0x20, 0x20, 0x70, // 1 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 0xF0, 0x80, 0xF0, 0x80, 0x80, // F }; int i = 0x50; for (auto const b : charset) this->memory[i++] = b; this->halted = true; } std::optional> load_binary_file(std::string path) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return std::nullopt; std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); auto buffer = new byte[size]; if (file.read((char *)buffer, size)) return std::make_pair(buffer, static_cast(size)); return std::nullopt; } enum class Screen { RomSelect, Emulator, }; struct FileInfo { fs::path path; int size; bool is_dir; }; void get_files(fs::path path, std::vector &list) { list.clear(); list.push_back(FileInfo { fs::path { ".." }, 0, true }); for (auto const &entry : fs::directory_iterator(path)) { bool dir = entry.is_directory(); int fs = 0; if (entry.is_regular_file()) { fs = static_cast(fs::file_size(entry.path())); if (fs > (0x1000 - 0x200)) continue; if (entry.path().filename().string() == "PARAM.SFO") continue; if (entry.path().filename().string() == "compile_commands.json") continue; if (entry.path().filename().string() == "cmake_install.cmake") continue; } else if (!dir) { continue; } if (entry.path().filename().string() == "CMakeFiles") continue; list.push_back(FileInfo { entry.path().filename(), fs, dir }); } std::sort(list.begin(), list.end(), [](FileInfo const &a, FileInfo const &b) { if (a.is_dir != b.is_dir) return a.is_dir > b.is_dir; return a.path.string() < b.path.string(); }); } fs::path resolve_path(fs::path const &path) { std::stack components; for (auto const &part : path) { if (part == "..") { if (!components.empty()) components.pop(); } else { components.push(part); } } fs::path resolved_path; while (!components.empty()) { resolved_path = components.top() / resolved_path; components.pop(); } if (resolved_path.empty()) resolved_path = fs::path { "./" }; return resolved_path; } CHIP8 g_chip8; int g_step_skips = 20; int g_artificial_delay = 1; int chip_thread(SceSize args, void *argp) { (void)args; (void)argp; for (;;) { g_chip8.keypad.update(); for (int i = 0; i < g_step_skips; i++) g_chip8.step(); std::this_thread::sleep_for(std::chrono::milliseconds(g_artificial_delay)); } } int main(void) { srand(time(NULL)); pspDebugScreenInit(); setup_callbacks(); Config cfg(toml::parse_file("config.toml")); g_chip8.keypad.mappings = cfg.mappings; scePowerUnlock(0); scePowerSetClockFrequency(333, 333, 166); g_fb = (u32 *)get_static_vram_texture(CHIP_W, CHIP_H, GU_PSM_8888); for (int x = 0; x < CHIP_W; x++) for (int y = 0; y < CHIP_H; y++) g_fb[x + CHIP_W * y] = 0xff0000ff; void *fbp0 = get_static_vram_buffer(BUF_WIDTH, SCR_HEIGHT, GU_PSM_8888); void *fbp1 = get_static_vram_buffer(BUF_WIDTH, SCR_HEIGHT, GU_PSM_8888); void *zbp = get_static_vram_buffer(BUF_WIDTH, SCR_HEIGHT, GU_PSM_4444); sceGuInit(); sceGuStart(GU_DIRECT, list); sceGuDrawBuffer(GU_PSM_8888, fbp0, BUF_WIDTH); sceGuDispBuffer(SCR_WIDTH, SCR_HEIGHT, fbp1, BUF_WIDTH); sceGuDepthBuffer(zbp, BUF_WIDTH); sceGuOffset(2048 - (SCR_WIDTH / 2), 2048 - (SCR_HEIGHT / 2)); sceGuViewport(2048, 2048, SCR_WIDTH, SCR_HEIGHT); sceGuDepthRange(65535, 0); sceGuScissor(0, 0, SCR_WIDTH, SCR_HEIGHT); sceGuEnable(GU_SCISSOR_TEST); sceGuAlphaFunc(GU_GREATER, 0, 0xff); sceGuEnable(GU_ALPHA_TEST); sceGuFrontFace(GU_CW); sceGuShadeModel(GU_SMOOTH); sceGuEnable(GU_CULL_FACE); sceGuEnable(GU_TEXTURE_2D); sceGuTexFunc(GU_TFX_MODULATE, GU_TCC_RGBA); sceGuTexFilter(GU_NEAREST, GU_NEAREST); sceGuTexWrap(GU_CLAMP, GU_CLAMP); sceGuFinish(); sceGuSync(0, 0); sceDisplayWaitVblankStart(); sceGuDisplay(1); sceCtrlSetSamplingCycle(0); sceCtrlSetSamplingMode(PSP_CTRL_MODE_ANALOG); std::string filename; auto screen = Screen::RomSelect; auto last = std::chrono::high_resolution_clock::now(); std::vector file_list; fs::path path { "." }; ssize_t menu_y = 0; get_files(path, file_list); SceUID chip_thread_id = sceKernelCreateThread("chip_thread", &chip_thread, 0x18, 0x10000, 0, nullptr); sceKernelStartThread(chip_thread_id, 0, nullptr); float t = 0; for (;;) { auto ctime = std::chrono::high_resolution_clock::now(); std::chrono::duration delta_time = ctime - last; last = ctime; float dt = delta_time.count(); t += dt; if (screen == Screen::Emulator) { static bool prev_start = false; if (g_chip8.keypad.start()) { if (g_chip8.keypad.second()) { if (!prev_start) g_chip8.halted = !g_chip8.halted; } else { g_chip8.halted = true; screen = Screen::RomSelect; } } prev_start = g_chip8.keypad.start(); } constexpr int MAX_SCREEN_X = 68; constexpr int MAX_SCREEN_Y = 33; if (t >= 1 / 60.0) { if (g_chip8.delay_timer) g_chip8.delay_timer--; t -= 1 / 60.0; } if (screen == Screen::RomSelect) { SceCtrlData pad; static SceCtrlData prev_pad = { 0, 0, 0, 0, { 0, 0, 0, 0, 0, 0 } }; sceCtrlPeekBufferPositive(&pad, 1); static bool is_fast_scrolling = false; static float t_input_repeat = 0.0f; t_input_repeat += dt; bool first_tap_up = (pad.Buttons & PSP_CTRL_UP) && !(prev_pad.Buttons & PSP_CTRL_UP); bool first_tap_down = (pad.Buttons & PSP_CTRL_DOWN) && !(prev_pad.Buttons & PSP_CTRL_DOWN); bool first_tap_triangle = (pad.Buttons & PSP_CTRL_TRIANGLE) && !(prev_pad.Buttons & PSP_CTRL_TRIANGLE); bool first_tap_square = (pad.Buttons & PSP_CTRL_SQUARE) && !(prev_pad.Buttons & PSP_CTRL_SQUARE); bool first_tap = first_tap_up || first_tap_down || first_tap_triangle || first_tap_square; if (first_tap) t_input_repeat = 0.0f; if (!is_fast_scrolling && t_input_repeat > 0.2f && (pad.Buttons & (PSP_CTRL_DOWN | PSP_CTRL_UP | PSP_CTRL_SQUARE | PSP_CTRL_TRIANGLE))) is_fast_scrolling = true; if (!(pad.Buttons & (PSP_CTRL_SQUARE | PSP_CTRL_TRIANGLE | PSP_CTRL_DOWN | PSP_CTRL_UP))) is_fast_scrolling = false; if (is_fast_scrolling || first_tap) { t_input_repeat = 0.0f; if (pad.Buttons & PSP_CTRL_DOWN) { menu_y++; if (menu_y >= file_list.size()) menu_y = file_list.size() - 1; } if (pad.Buttons & PSP_CTRL_UP) { menu_y--; if (menu_y < 0) menu_y = 0; } if (pad.Buttons & PSP_CTRL_TRIANGLE) { g_step_skips += (pad.Buttons & PSP_CTRL_LTRIGGER) ? 1 : -1; if (g_step_skips < 1) g_step_skips = 1; } if (pad.Buttons & PSP_CTRL_SQUARE) { g_artificial_delay += (pad.Buttons & PSP_CTRL_LTRIGGER) ? 1 : -1; if (g_artificial_delay < 1) g_artificial_delay = 1; } } if ((pad.Buttons & PSP_CTRL_CROSS) && !(prev_pad.Buttons & PSP_CTRL_CROSS)) { fs::path newp = path / file_list[menu_y].path; if (fs::is_directory(newp)) { path = resolve_path(newp); get_files(path, file_list); } else { auto data = load_binary_file(newp.c_str()); if (data.has_value()) { auto [rom, rom_len] = data.value(); filename = newp.filename().string(); screen = Screen::Emulator; g_chip8.load(rom, rom_len); g_chip8.message = ""; } } menu_y = 0; } prev_pad = pad; } sceGuStart(GU_DIRECT, list); sceGuClearColor(0xff000000); sceGuClear(GU_COLOR_BUFFER_BIT); sceGumMatrixMode(GU_PROJECTION); sceGumLoadIdentity(); sceGumOrtho(0, 480, 272, 0, -1, 1); sceGumMatrixMode(GU_VIEW); sceGumLoadIdentity(); sceGumMatrixMode(GU_MODEL); sceGumLoadIdentity(); if (screen == Screen::Emulator) { constexpr int SCALE = 7; constexpr int SX = SCR_WIDTH / 2 - CHIP_W * SCALE / 2; constexpr int SY = SCR_HEIGHT / 2 - CHIP_H * SCALE / 2; render_texture(SX, SY, CHIP_W * SCALE, CHIP_H * SCALE, g_fb, CHIP_W, CHIP_H); } sceGuFinish(); sceGuSync(0, 0); pspDebugScreenSetOffset((int)fbp0); pspDebugScreenSetXY(0, 0); if (screen == Screen::RomSelect) { pspDebugScreenSetTextColor(0xFFFFFFFF); pspDebugScreenPrintf("SK: %d D: %dms Current path: %s\n", g_step_skips, g_artificial_delay, path.c_str()); pspDebugScreenSetTextColor(0xFF222222); for (int i = 0; i < MAX_SCREEN_X; i++) pspDebugScreenPrintf("="); static ssize_t scroll_off = 0; if (menu_y - scroll_off >= MAX_SCREEN_Y - 3) scroll_off++; if (menu_y < scroll_off) scroll_off--; if (scroll_off < 0) scroll_off = 0; for (ssize_t i = scroll_off; i < file_list.size() && i < scroll_off + MAX_SCREEN_Y - 3; i++) { auto &entry = file_list[i]; auto &path = entry.path; if (i == menu_y) pspDebugScreenSetTextColor(0xFF2b7311); else { if (entry.is_dir) pspDebugScreenSetTextColor(0xFF750e0b); else pspDebugScreenSetTextColor(0xFFFFFFFF); } if (entry.is_dir) pspDebugScreenPrintf("%-68s", path.c_str()); else pspDebugScreenPrintf("%-63s%5d", path.c_str(), entry.size); } pspDebugScreenSetXY(0, MAX_SCREEN_Y - 1); pspDebugScreenSetTextColor(0xFF222222); for (int i = 0; i < MAX_SCREEN_X; i++) pspDebugScreenPrintf("="); pspDebugScreenSetTextColor(0xFFFFFFFF); pspDebugScreenPrintf( "Select(X), Move(UP/DOWN), DecHold(LT), StepSkips(/\\), Delay([])"); } else { pspDebugScreenPrintf("%sMessage: %s Map: %s SK: %d D: %dms File: %s", g_chip8.halted ? "HALTED " : "", g_chip8.message.c_str(), g_chip8.keypad.map_name().c_str(), g_step_skips, g_artificial_delay, filename.c_str()); } sceDisplayWaitVblank(); fbp0 = sceGuSwapBuffers(); } sceGuTerm(); sceKernelExitGame(); }