| #include <cstring> |
| |
| #include "pico/stdlib.h" |
| #include "hardware/flash.h" |
| #include "hardware/sync.h" |
| #include "pico/multicore.h" |
| |
| #ifdef PICO_RP2350 |
| #include "hardware/structs/qmi.h" |
| #include "hardware/xip_cache.h" |
| #endif |
| |
| #include "blit_launch.hpp" |
| #include "file.hpp" |
| #include "overlay.hpp" |
| |
| #include "engine/engine.hpp" |
| #include "engine/file.hpp" |
| |
| #ifdef PICO_RP2350 |
| #define DEVICE_ID BlitDevice::RP2350 |
| #define FLASH_BASE XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE |
| #else |
| #define DEVICE_ID BlitDevice::RP2040 |
| #define FLASH_BASE XIP_NOCACHE_NOALLOC_BASE |
| #endif |
| |
| // code related to blit files and launching |
| |
| extern int (*do_tick)(uint32_t time); |
| |
| void disable_user_code(); |
| |
| extern bool core1_started; |
| |
| static uint32_t requested_launch_offset = 0; |
| static uint32_t current_game_offset = 0; |
| |
| static uint32_t get_installed_file_size(uint32_t offset) { |
| auto header = (BlitGameHeader *)(FLASH_BASE + offset); |
| |
| // check header magic + device |
| if(header->magic != blit_game_magic || header->device_id != DEVICE_ID) |
| return 0; |
| |
| auto size = header->end; |
| |
| // check metadata |
| auto meta_offset = offset + size; |
| if(memcmp((char *)(FLASH_BASE + meta_offset), "BLITMETA", 8) == 0) { |
| // add metadata size |
| size += *(uint16_t *)(FLASH_BASE + meta_offset + 8) + 10; |
| } |
| |
| return size; |
| } |
| |
| static uint32_t calc_num_blocks(uint32_t size) { |
| return (size - 1) / game_block_size + 1; |
| } |
| |
| static uint32_t find_flash_offset(uint32_t requested_size) { |
| uint32_t free_off = 0; // 0 is invalid as that's where the loader is |
| |
| // FIXME: avoid flash storage |
| |
| for(uint32_t off = 256 * 1024; off < PICO_FLASH_SIZE_BYTES;) { |
| auto size = get_installed_file_size(off); |
| |
| if(!size) { |
| // empty, store offset |
| if(!free_off) |
| free_off = off; |
| |
| off += game_block_size; |
| continue; |
| } |
| |
| if(free_off) { |
| // end of free space, check if large enough |
| auto found_space = off - free_off; |
| |
| if(found_space >= requested_size) |
| return free_off; |
| |
| free_off = 0; |
| } |
| |
| // skip to end |
| off += calc_num_blocks(size) * game_block_size; |
| } |
| |
| // last chance |
| if(free_off && PICO_FLASH_SIZE_BYTES - free_off >= requested_size) |
| return free_off; |
| |
| return 0; |
| } |
| |
| static bool read_file_metadata(void *file, RawMetadata &meta, RawTypeMetadata &type_meta) { |
| // read header and check magic |
| BlitGameHeader header; |
| if(read_file(file, 0, sizeof(header), (char *)&header) != sizeof(header)) |
| return false; |
| |
| if(header.magic != blit_game_magic) |
| return false; |
| |
| // read and check metadata header |
| auto meta_offset = header.end; |
| |
| char meta_header[10]; |
| if(read_file(file, meta_offset, 10, meta_header) != 10) |
| return false; |
| |
| if(memcmp(meta_header, "BLITMETA", 8) != 0) |
| return false; |
| |
| // read the reset of the metadata header |
| if(read_file(file, meta_offset + 10, sizeof(RawMetadata), (char *)&meta) != sizeof(RawMetadata)) |
| return false; |
| |
| // check for type data |
| meta_offset += 10 + sizeof(RawMetadata); |
| if(read_file(file, meta_offset, 8, meta_header) != 8) |
| return false; |
| |
| if(memcmp(meta_header, "BLITTYPE", 8) == 0) { |
| if(read_file(file, meta_offset + 8, sizeof(RawTypeMetadata), (char *)&type_meta) != sizeof(RawTypeMetadata)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static uint32_t find_installed_blit(RawMetadata &meta) { |
| for(uint32_t off = 0; off < PICO_FLASH_SIZE_BYTES;) { |
| auto size = get_installed_file_size(off); |
| |
| if(!size) { |
| off += game_block_size; |
| continue; |
| } |
| |
| auto header = (BlitGameHeader *)(FLASH_BASE + off); |
| auto flash_meta = (RawMetadata *)(FLASH_BASE + off + header->end + 10); |
| |
| // check CRC ant title |
| if(meta.crc32 == flash_meta->crc32 && strcmp(meta.title, flash_meta->title) == 0) { |
| return off; |
| } |
| |
| off += calc_num_blocks(size) * game_block_size; |
| } |
| |
| return ~0u; |
| } |
| |
| static bool cleanup_duplicates(RawMetadata &meta, RawTypeMetadata &type_meta, uint32_t new_offset) { |
| bool ret = false; |
| for(uint32_t off = 0; off < PICO_FLASH_SIZE_BYTES;) { |
| auto size = get_installed_file_size(off); |
| |
| if(!size) { |
| off += game_block_size; |
| continue; |
| } |
| |
| auto header = (BlitGameHeader *)(FLASH_BASE + off); |
| auto flash_meta = (RawMetadata *)(FLASH_BASE + off + header->end + 10); |
| |
| // check title and author, ignore the current copy of the game |
| if(off != new_offset && strcmp(meta.title, flash_meta->title) == 0 && strcmp(meta.author, flash_meta->author) == 0) { |
| erase_game(off); |
| |
| if(off == current_game_offset) |
| ret = true; |
| } |
| |
| off += calc_num_blocks(size) * game_block_size; |
| } |
| |
| return ret; |
| } |
| |
| // 32blit API |
| |
| RawMetadata *get_running_game_metadata() { |
| #ifdef BUILD_LOADER |
| if(!current_game_offset) |
| return nullptr; |
| |
| auto game_ptr = reinterpret_cast<uint8_t *>(FLASH_BASE + current_game_offset); |
| |
| auto header = reinterpret_cast<BlitGameHeader *>(game_ptr); |
| |
| if(header->magic == blit_game_magic) { |
| auto end_ptr = game_ptr + header->end; |
| if(memcmp(end_ptr, "BLITMETA", 8) == 0) |
| return reinterpret_cast<RawMetadata *>(end_ptr + 10); |
| } |
| |
| #endif |
| return nullptr; |
| } |
| |
| bool launch_file(const char *path) { |
| uint32_t flash_offset = ~0u; |
| |
| if(strncmp(path, "flash:/", 7) == 0) // from flash |
| flash_offset = atoi(path + 7) * game_block_size; |
| else { |
| // from storage |
| auto file = open_file(path, blit::OpenMode::read); |
| |
| if(!file) |
| return false; |
| |
| // read file metadata and try to find matching installed gat |
| RawMetadata meta; |
| RawTypeMetadata type_meta = {}; |
| |
| if(read_file_metadata(file, meta, type_meta)) |
| flash_offset = find_installed_blit(meta); |
| |
| // flash if not found |
| if(flash_offset == ~0u) { |
| BlitWriter writer; |
| |
| uint32_t file_offset = 0; |
| uint32_t len = get_file_length(file); |
| |
| writer.init(len); |
| |
| // read in small chunks |
| uint8_t buf[FLASH_PAGE_SIZE]; |
| |
| while(file_offset < len) { |
| auto bytes_read = read_file(file, file_offset, FLASH_PAGE_SIZE, (char *)buf); |
| if(bytes_read <= 0) |
| break; |
| |
| if(!writer.write(buf, bytes_read)) |
| break; |
| |
| file_offset += bytes_read; |
| } |
| |
| close_file(file); |
| |
| // didn't write everything, fail launch |
| if(writer.get_remaining() > 0) |
| return false; |
| |
| flash_offset = writer.get_flash_offset(); |
| |
| cleanup_duplicates(meta, type_meta, flash_offset); |
| } else |
| close_file(file); |
| } |
| |
| auto header = (BlitGameHeader *)(FLASH_BASE + flash_offset); |
| // check header magic + device |
| if(header->magic != blit_game_magic || header->device_id != DEVICE_ID) |
| return false; |
| |
| if(!header->init || !header->render || !header->tick) |
| return false; |
| |
| requested_launch_offset = flash_offset; |
| return true; |
| } |
| |
| blit::CanLaunchResult can_launch(const char *path) { |
| #ifdef BUILD_LOADER |
| if(strncmp(path, "flash:/", 7) == 0) { |
| // assume anything flashed is compatible for now |
| return blit::CanLaunchResult::Success; |
| } |
| |
| // get the extension |
| std::string_view sv(path); |
| auto last_dot = sv.find_last_of('.'); |
| auto ext = last_dot == std::string::npos ? "" : std::string(sv.substr(last_dot + 1)); |
| for(auto &c : ext) |
| c = tolower(c); |
| |
| if(ext == "blit") { |
| BlitGameHeader header; |
| auto file = open_file(path, blit::OpenMode::read); |
| |
| if(!file) |
| return blit::CanLaunchResult::InvalidFile; |
| |
| auto bytes_read = read_file(file, 0, sizeof(header), (char *)&header); |
| |
| if(bytes_read == sizeof(header) && header.magic == blit_game_magic && header.device_id == DEVICE_ID) { |
| close_file(file); |
| return blit::CanLaunchResult::Success; |
| } |
| |
| close_file(file); |
| return blit::CanLaunchResult::IncompatibleBlit; |
| } |
| #endif |
| |
| return blit::CanLaunchResult::UnknownType; |
| } |
| |
| void launch_pre_init() { |
| // reset api state before launching new game |
| blit::api_data.buttons = {0, 0, 0}; |
| |
| blit::api_data.LED = {0, 0, 0}; |
| blit::api_data.vibration = 0.0f; |
| blit::api_data.message_received = nullptr; |
| blit::api_data.i2c_completed = nullptr; |
| |
| for(int i = 0; i < CHANNEL_COUNT; i++) |
| blit::api.channels[i] = blit::AudioChannel(); |
| } |
| |
| void delayed_launch() { |
| if(!requested_launch_offset) |
| return; |
| |
| auto header = (BlitGameHeader *)(FLASH_BASE + requested_launch_offset); |
| |
| #ifdef PICO_RP2350 |
| uint32_t header_offset = *(uint32_t *)(FLASH_BASE + requested_launch_offset + sizeof(BlitGameHeader)); |
| if(header_offset != requested_launch_offset) { |
| // setup translation |
| uint32_t size = 4 * 1024 * 1024; // TODO: use (rounded) blit size |
| qmi_hw->atrans[1] = (size >> 12) << QMI_ATRANS1_SIZE_LSB |
| | (requested_launch_offset >> 12) << QMI_ATRANS1_BASE_LSB; |
| |
| // invalidate cache |
| xip_cache_invalidate_range(4 * 1024 * 1024, size); |
| |
| // FIXME: handle previous blit also using translation on failure |
| } |
| #endif |
| |
| // save in case launch fails |
| uint32_t last_game_offset = current_game_offset; |
| |
| current_game_offset = requested_launch_offset; |
| requested_launch_offset = 0; |
| |
| launch_pre_init(); |
| |
| if(!header->init(0)) { |
| blit::debugf("failed to init game!\n"); |
| current_game_offset = last_game_offset; |
| return; |
| } |
| |
| blit::render = header->render; |
| do_tick = header->tick; |
| } |
| |
| void list_installed_games(std::function<void(const uint8_t *, uint32_t, uint32_t)> callback) { |
| for(uint32_t off = 0; off < PICO_FLASH_SIZE_BYTES;) { |
| auto size = get_installed_file_size(off); |
| |
| if(!size) { |
| off += game_block_size; |
| continue; |
| } |
| |
| callback((const uint8_t *)(FLASH_BASE + off), off / game_block_size, size); |
| |
| off += calc_num_blocks(size) * game_block_size; |
| } |
| } |
| |
| void erase_game(uint32_t offset) { |
| #ifdef BUILD_LOADER |
| // check alignment |
| if(offset & (game_block_size - 1)) |
| return; |
| |
| // check in bounds |
| // TODO: prevent erasing fs if flash storage is used? |
| if(offset >= PICO_FLASH_SIZE_BYTES) |
| return; |
| |
| auto size = get_installed_file_size(offset); |
| |
| // fall back to one block if size unknown |
| auto num_blocks = size == 0 ? 1 : calc_num_blocks(size); |
| |
| // we can't display progress unless we break up the erase... |
| set_render_overlay_enabled(true); |
| set_overlay_message("Erasing flash..."); |
| set_overlay_progress(0, 0); |
| // make sure the message is displayed before we disable interrupts/core1 |
| overlay_try_render(true); |
| |
| // do erase |
| auto status = save_and_disable_interrupts(); |
| |
| if(core1_started) |
| multicore_lockout_start_blocking(); // pause core1 |
| |
| // the real erase size is smaller than the one baked into the API... |
| static_assert(game_block_size % FLASH_SECTOR_SIZE == 0); |
| flash_range_erase(offset, num_blocks * game_block_size); |
| |
| if(core1_started) |
| multicore_lockout_end_blocking(); // resume core1 |
| |
| restore_interrupts(status); |
| |
| set_render_overlay_enabled(false); |
| #endif |
| } |
| |
| // .blit file writer |
| void BlitWriter::init(uint32_t file_len) { |
| this->file_len = file_len; |
| file_offset = flash_offset = 0; |
| } |
| |
| bool BlitWriter::write(const uint8_t *buf, uint32_t len) { |
| if(!flash_offset) { |
| if(!prepare_write(buf)) |
| return false; |
| } |
| |
| if(file_offset >= file_len) |
| return false; |
| |
| // write page |
| auto status = save_and_disable_interrupts(); |
| |
| if(core1_started) |
| multicore_lockout_start_blocking(); // pause core1 |
| |
| // assuming len <= page size and buf size == page size |
| flash_range_program(flash_offset + file_offset, buf, FLASH_PAGE_SIZE); |
| |
| if(core1_started) |
| multicore_lockout_end_blocking(); // resume core1 |
| |
| restore_interrupts(status); |
| |
| file_offset += len; |
| |
| return true; |
| } |
| |
| uint32_t BlitWriter::get_offset() const { |
| return file_offset; |
| } |
| |
| uint32_t BlitWriter::get_length() const { |
| return file_len; |
| } |
| |
| uint32_t BlitWriter::get_remaining() const { |
| return file_len - file_offset; |
| } |
| |
| uint32_t BlitWriter::get_flash_offset() const { |
| return flash_offset; |
| } |
| |
| bool BlitWriter::prepare_write(const uint8_t *buf) { |
| auto header = (BlitGameHeader *)buf; |
| |
| if(header->magic != blit_game_magic || header->device_id != DEVICE_ID) { |
| blit::debugf("Invalid blit header!"); |
| return false; |
| } |
| |
| // currently non-relocatable, so base address is stored after header |
| flash_offset = *(uint32_t *)(buf + sizeof(BlitGameHeader)); |
| flash_offset &= 0xFFFFFF; |
| |
| #ifdef PICO_RP2350 |
| if(flash_offset == 4 * 1024 * 1024) { |
| // we can use address translation for this, so flash in any free space |
| flash_offset = find_flash_offset(file_len); |
| } |
| #endif |
| |
| disable_user_code(); |
| |
| // erase flash |
| auto status = save_and_disable_interrupts(); |
| |
| if(core1_started) |
| multicore_lockout_start_blocking(); // pause core1 |
| |
| auto erase_size = ((file_len - 1) / FLASH_SECTOR_SIZE) + 1; |
| flash_range_erase(flash_offset, erase_size * FLASH_SECTOR_SIZE); |
| |
| if(core1_started) |
| multicore_lockout_end_blocking(); // resume core1 |
| |
| restore_interrupts(status); |
| |
| return true; |
| } |