diff --git a/CMakeLists.txt b/CMakeLists.txt
index c366c76..a09ca6e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -166,6 +166,7 @@ target_include_directories(MMRecomp PRIVATE
     ${CMAKE_SOURCE_DIR}/lib/RT64-HLE/src/rhi
     ${CMAKE_SOURCE_DIR}/lib/RT64-HLE/src/render
     ${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/include
+    ${CMAKE_SOURCE_DIR}/lib/lib/nativefiledialog-extended/src/include
     ${CMAKE_BINARY_DIR}/shaders
 )
 
diff --git a/assets/launcher.rml b/assets/launcher.rml
index 12f1864..1748505 100644
--- a/assets/launcher.rml
+++ b/assets/launcher.rml
@@ -12,7 +12,7 @@
 		</style>
 	</head>
 	<body>
-		<div id="window">
+		<div id="window" data-model="launcher_model">
 			<div class="launcher">
 				<div class="launcher__vertical-split">
 					<div class="launcher__title-quadrant">
@@ -35,7 +35,11 @@
 						</button>
 					</div>
 					<div class="launcher__content-quadrant">
-						<button onclick="start_game" class="menu-list-item menu-list-item--right" autofocus>
+						<button data-if="!mm_rom_valid" onclick="select_rom" class="menu-list-item menu-list-item--right" autofocus>
+							<div class="menu-list-item__bullet">•</div>
+							<div class="menu-list-item__label">Select ROM</div>
+						</button>
+						<button data-if="mm_rom_valid" onclick="start_game" class="menu-list-item menu-list-item--right" autofocus>
 							<div class="menu-list-item__bullet">•</div>
 							<div class="menu-list-item__label">Start game</div>
 						</button>
diff --git a/include/recomp_config.h b/include/recomp_config.h
index c15604b..9310d53 100644
--- a/include/recomp_config.h
+++ b/include/recomp_config.h
@@ -1,6 +1,7 @@
 #ifndef __RECOMP_CONFIG_H__
 #define __RECOMP_CONFIG_H__
 
+#include <filesystem>
 #include <string_view>
 #include "../ultramodern/config.hpp"
 
@@ -12,6 +13,8 @@ namespace recomp {
     
     void reset_input_bindings();
     void reset_graphics_options();
+
+    std::filesystem::path get_app_folder_path();
 };
 
 #endif 
\ No newline at end of file
diff --git a/include/recomp_game.h b/include/recomp_game.h
new file mode 100644
index 0000000..13f0061
--- /dev/null
+++ b/include/recomp_game.h
@@ -0,0 +1,39 @@
+#ifndef __RECOMP_GAME__
+#define __RECOMP_GAME__
+
+#include <vector>
+#include <filesystem>
+
+#include "recomp.h"
+#include "../ultramodern/ultramodern.hpp"
+#include "rt64_layer.h"
+
+namespace recomp {
+	enum class Game {
+		OoT,
+		MM,
+		None,
+		Quit
+	};
+	enum class RomValidationError {
+		Good,
+		FailedToOpen,
+		NotARom,
+		IncorrectRom,
+		NotYet,
+		IncorrectVersion,
+		OtherError
+	};
+	void check_all_stored_roms();
+	bool load_stored_rom(Game game);
+	RomValidationError select_rom(const std::filesystem::path& rom_path, Game game);
+	bool is_rom_valid(Game game);
+	bool is_rom_loaded();
+	void set_rom_contents(std::vector<uint8_t>&& new_rom);
+	void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes);
+	void start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks);
+	void start_game(Game game);
+	void message_box(const char* message);
+}
+
+#endif
diff --git a/include/rt64_layer.h b/include/rt64_layer.h
index fe2550a..b5df376 100644
--- a/include/rt64_layer.h
+++ b/include/rt64_layer.h
@@ -12,7 +12,7 @@ namespace ultramodern {
     struct WindowHandle;
 }
 
-RT64::Application* RT64Init(uint8_t* rom, uint8_t* rdram, ultramodern::WindowHandle window_handle, bool developer_mode);
+RT64::Application* RT64Init(uint8_t* rdram, ultramodern::WindowHandle window_handle, bool developer_mode);
 void RT64UpdateConfig(RT64::Application* application, const ultramodern::GraphicsConfig& old_config, const ultramodern::GraphicsConfig& new_config);
 void RT64EnableInstantPresent(RT64::Application* application);
 void RT64SendDL(uint8_t* rdram, const OSTask* task);
diff --git a/patches/camera_transform_tagging.c b/patches/camera_transform_tagging.c
index 0a6e533..8ba4aff 100644
--- a/patches/camera_transform_tagging.c
+++ b/patches/camera_transform_tagging.c
@@ -165,7 +165,6 @@ void View_Apply(View* view, s32 mask) {
     // Force skipping interpolation if the previous view was kaleido and this one isn't.
     if (prev_in_kaleido && !in_kaleido) {
         camera_skip_interpolation_forced = true;
-        recomp_printf("exited kaleido\n");
     }
 
     // @recomp Apply camera interpolation overrides.
diff --git a/src/game/config.cpp b/src/game/config.cpp
index 6de4d16..8b227e1 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -69,7 +69,7 @@ namespace recomp {
     }
 }
 
-std::filesystem::path get_config_folder_path() {
+std::filesystem::path recomp::get_app_folder_path() {
    std::filesystem::path recomp_dir{};
 
 #if defined(_WIN32)
@@ -232,7 +232,7 @@ void load_controls_config(const std::filesystem::path& path) {
 }
 
 void recomp::load_config() {
-    std::filesystem::path recomp_dir = get_config_folder_path();
+    std::filesystem::path recomp_dir = recomp::get_app_folder_path();
     std::filesystem::path graphics_path = recomp_dir / graphics_filename;
     std::filesystem::path controls_path = recomp_dir / controls_filename;
 
@@ -254,7 +254,7 @@ void recomp::load_config() {
 }
 
 void recomp::save_config() {
-    std::filesystem::path recomp_dir = get_config_folder_path();
+    std::filesystem::path recomp_dir = recomp::get_app_folder_path();
 
     if (recomp_dir.empty()) {
         return;
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 73bf06c..f73bf78 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -20,6 +20,7 @@
 #include "recomp_ui.h"
 #include "recomp_input.h"
 #include "recomp_config.h"
+#include "recomp_game.h"
 
 #ifdef _WIN32
 #define WIN32_LEAN_AND_MEAN
@@ -229,13 +230,12 @@ int main(int argc, char** argv) {
     std::setlocale(LC_ALL, "en_US.UTF-8");
 #endif
 
-    printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
+    //printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
 
     // Initialize SDL audio and set the output frequency.
     SDL_InitSubSystem(SDL_INIT_AUDIO);
     reset_audio(48000);
 
-    init();
     recomp::load_config();
 
     ultramodern::gfx_callbacks_t gfx_callbacks{
@@ -255,7 +255,7 @@ int main(int argc, char** argv) {
         .get_input = recomp::get_n64_input,
     };
 
-    ultramodern::start({}, audio_callbacks, input_callbacks, gfx_callbacks);
+    recomp::start({}, audio_callbacks, input_callbacks, gfx_callbacks);
 
     return EXIT_SUCCESS;
 }
diff --git a/src/recomp/pi.cpp b/src/recomp/pi.cpp
index 0db725f..5ee5796 100644
--- a/src/recomp/pi.cpp
+++ b/src/recomp/pi.cpp
@@ -3,9 +3,20 @@
 #include <array>
 #include <cstring>
 #include "recomp.h"
+#include "recomp_game.h"
 #include "../ultramodern/ultra64.h"
 #include "../ultramodern/ultramodern.hpp"
 
+static std::vector<uint8_t> rom;
+
+bool recomp::is_rom_loaded() {
+    return !rom.empty();
+}
+
+void recomp::set_rom_contents(std::vector<uint8_t>&& new_rom) {
+    rom = std::move(new_rom);
+}
+
 // Flashram occupies the same physical address as sram, but that issue is avoided because libultra exposes
 // a high-level interface for flashram. Because that high-level interface is reimplemented, low level accesses
 // that involve physical addresses don't need to be handled for flashram.
@@ -20,9 +31,6 @@ constexpr uint32_t phys_to_k1(uint32_t addr) {
     return addr | 0xA0000000;
 }
 
-extern std::unique_ptr<uint8_t[]> rom;
-extern size_t rom_size;
-
 extern "C" void osCartRomInit_recomp(uint8_t* rdram, recomp_context* ctx) {
     OSPiHandle* handle = TO_PTR(OSPiHandle, ultramodern::cart_handle);
     handle->type = 0; // cart
@@ -36,14 +44,14 @@ extern "C" void osCreatePiManager_recomp(uint8_t* rdram, recomp_context* ctx) {
     ;
 }
 
-void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes) {
+void recomp::do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes) {
     // TODO use word copies when possible
 
     // TODO handle misaligned DMA
     assert((physical_addr & 0x1) == 0 && "Only PI DMA from aligned ROM addresses is currently supported");
     assert((ram_address & 0x7) == 0 && "Only PI DMA to aligned RDRAM addresses is currently supported");
     assert((num_bytes & 0x1) == 0 && "Only PI DMA with aligned sizes is currently supported");
-    uint8_t* rom_addr = rom.get() + physical_addr - rom_base;
+    uint8_t* rom_addr = rom.data() + physical_addr - rom_base;
     for (size_t i = 0; i < num_bytes; i++) {
         MEM_B(i, ram_address) = *rom_addr;
         rom_addr++;
@@ -110,7 +118,7 @@ void do_dma(uint8_t* rdram, PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t phy
     if (direction == 0) {
         if (physical_addr >= rom_base) {
             // read cart rom
-            do_rom_read(rdram, rdram_address, physical_addr, size);
+            recomp::do_rom_read(rdram, rdram_address, physical_addr, size);
 
             // Send a message to the mq to indicate that the transfer completed
             osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
@@ -181,7 +189,7 @@ extern "C" void osEPiReadIo_recomp(uint8_t * rdram, recomp_context * ctx) {
 
     if (physical_addr > rom_base) {
         // cart rom
-        do_rom_read(rdram, dramAddr, physical_addr, sizeof(uint32_t));
+        recomp::do_rom_read(rdram, dramAddr, physical_addr, sizeof(uint32_t));
     } else {
         // sram
         assert(false && "SRAM ReadIo unimplemented");
diff --git a/src/recomp/recomp.cpp b/src/recomp/recomp.cpp
index d7271f1..00435f8 100644
--- a/src/recomp/recomp.cpp
+++ b/src/recomp/recomp.cpp
@@ -7,9 +7,13 @@
 #include <memory>
 #include <cmath>
 #include <unordered_map>
+#include <unordered_set>
 #include <fstream>
 #include <iostream>
 #include "recomp.h"
+#include "recomp_game.h"
+#include "recomp_config.h"
+#include "xxHash/xxh3.h"
 #include "../ultramodern/ultramodern.hpp"
 
 #ifdef _WIN32
@@ -28,13 +32,204 @@ constexpr uint32_t byteswap(uint32_t val) {
 }
 #endif
 
-extern "C" void _bzero(uint8_t* rdram, recomp_context* ctx) {
-    gpr start_addr = ctx->r4;
-    gpr size = ctx->r5;
+struct RomEntry {
+    uint64_t xxhash3_value;
+    std::u8string stored_filename;
+    std::string internal_rom_name;
+};
 
-    for (uint32_t i = 0; i < size; i++) {
-        MEM_B(start_addr, i) = 0;
+const std::unordered_map<recomp::Game, RomEntry> game_roms {
+    { recomp::Game::MM, { 0xEF18B4A9E2386169ULL, u8"mm.n64.us.1.0.z64", "ZELDA MAJORA'S MASK" }},
+};
+
+bool check_hash(const std::vector<uint8_t>& rom_data, uint64_t expected_hash) {
+    uint64_t calculated_hash = XXH3_64bits(rom_data.data(), rom_data.size());
+
+    return calculated_hash == expected_hash;
+}
+
+std::vector<uint8_t> read_file(const std::filesystem::path& path) {
+    std::vector<uint8_t> ret;
+
+    std::ifstream file{ path, std::ios::binary};
+
+    if (file.good()) {
+        file.seekg(0, std::ios::end);
+        ret.resize(file.tellg());
+        file.seekg(0, std::ios::beg);
+
+        file.read(reinterpret_cast<char*>(ret.data()), ret.size());
     }
+
+    return ret;
+}
+
+bool write_file(const std::filesystem::path& path, const std::vector<uint8_t>& data) {
+    std::ofstream out_file{ path, std::ios::binary };
+
+    if (!out_file.good()) {
+        return false;
+    }
+
+    out_file.write(reinterpret_cast<const char*>(data.data()), data.size());
+
+    return true;
+}
+
+bool check_stored_rom(const RomEntry& game_entry) {
+
+    std::vector stored_rom_data = read_file(recomp::get_app_folder_path() / game_entry.stored_filename);
+
+    if (!check_hash(stored_rom_data, game_entry.xxhash3_value)) {
+        // Incorrect hash, remove the stored ROM file if it exists.
+        std::filesystem::remove(recomp::get_app_folder_path() / game_entry.stored_filename);
+        return false;
+    }
+
+    return true;
+}
+
+static std::unordered_set<recomp::Game> valid_game_roms;
+
+bool recomp::is_rom_valid(recomp::Game game) {
+    return valid_game_roms.contains(game);
+}
+
+void recomp::check_all_stored_roms() {
+    for (const auto& cur_rom_entry: game_roms) {
+        if (check_stored_rom(cur_rom_entry.second)) {
+            valid_game_roms.insert(cur_rom_entry.first);
+        }
+    }
+}
+
+bool recomp::load_stored_rom(recomp::Game game) {
+    auto find_it = game_roms.find(game);
+
+    if (find_it == game_roms.end()) {
+        return false;
+    }
+    
+    std::vector<uint8_t> stored_rom_data = read_file(recomp::get_app_folder_path() / find_it->second.stored_filename);
+
+    if (!check_hash(stored_rom_data, find_it->second.xxhash3_value)) {
+        // The ROM no longer has the right hash, delete it.
+        std::filesystem::remove(recomp::get_app_folder_path() / find_it->second.stored_filename);
+        return false;
+    }
+
+    recomp::set_rom_contents(std::move(stored_rom_data));
+    return true;
+}
+
+const std::array<uint8_t, 4> first_rom_bytes { 0x80, 0x37, 0x12, 0x40 };
+
+enum class ByteswapType {
+    NotByteswapped,
+    Byteswapped4,
+    Byteswapped2,
+    Invalid
+};
+
+ByteswapType check_rom_start(const std::vector<uint8_t>& rom_data) {
+    if (rom_data.size() < 4) {
+        return ByteswapType::Invalid;
+    }
+
+    bool matched_all = true;
+
+    auto check_match = [&](uint8_t index0, uint8_t index1, uint8_t index2, uint8_t index3) {
+        return
+            rom_data[0] == first_rom_bytes[index0] &&
+            rom_data[1] == first_rom_bytes[index1] &&
+            rom_data[2] == first_rom_bytes[index2] &&
+            rom_data[3] == first_rom_bytes[index3];
+    };
+
+    // Check if the ROM is already in the correct byte order.
+    if (check_match(0,1,2,3)) {
+        return ByteswapType::NotByteswapped;
+    }
+
+    // Check if the ROM has been byteswapped in groups of 4 bytes.
+    if (check_match(3,2,1,0)) {
+        return ByteswapType::Byteswapped4;
+    }
+
+    // Check if the ROM has been byteswapped in groups of 2 bytes.
+    if (check_match(1,0,3,2)) {
+        return ByteswapType::Byteswapped2;
+    }
+
+    // No match found.
+    return ByteswapType::Invalid;
+}
+
+void byteswap_data(std::vector<uint8_t>& rom_data, size_t index_xor) {
+    for (size_t rom_pos = 0; rom_pos < rom_data.size(); rom_pos += 4) {
+        uint8_t temp0 = rom_data[rom_pos + 0];
+        uint8_t temp1 = rom_data[rom_pos + 1];
+        uint8_t temp2 = rom_data[rom_pos + 2];
+        uint8_t temp3 = rom_data[rom_pos + 3];
+
+        rom_data[rom_pos + (0 ^ index_xor)] = temp0;
+        rom_data[rom_pos + (1 ^ index_xor)] = temp1;
+        rom_data[rom_pos + (2 ^ index_xor)] = temp2;
+        rom_data[rom_pos + (3 ^ index_xor)] = temp3;
+    }
+}
+
+recomp::RomValidationError recomp::select_rom(const std::filesystem::path& rom_path, Game game) {
+    auto find_it = game_roms.find(game);
+
+    if (find_it == game_roms.end()) {
+        return recomp::RomValidationError::OtherError;
+    }
+
+    const RomEntry& game_entry = find_it->second;
+
+    std::vector<uint8_t> rom_data = read_file(rom_path);
+
+    if (rom_data.empty()) {
+        return recomp::RomValidationError::FailedToOpen;
+    }
+
+    // Pad the rom to the nearest multiple of 4 bytes.
+    rom_data.resize((rom_data.size() + 3) & ~3);
+
+    ByteswapType byteswap_type = check_rom_start(rom_data);
+
+    switch (byteswap_type) {
+        case ByteswapType::Invalid:
+            return recomp::RomValidationError::NotARom;
+        case ByteswapType::Byteswapped2:
+            byteswap_data(rom_data, 1);
+            break;
+        case ByteswapType::Byteswapped4:
+            byteswap_data(rom_data, 3);
+            break;
+        case ByteswapType::NotByteswapped:
+            break;
+    }
+
+    if (!check_hash(rom_data, game_entry.xxhash3_value)) {
+        const std::string_view name{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, game_entry.internal_rom_name.size()};
+        if (name == game_entry.internal_rom_name) {
+            return recomp::RomValidationError::IncorrectVersion;
+        }
+        else {
+            if (game == recomp::Game::MM && std::string_view{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, 19 } == "THE LEGEND OF ZELDA") {
+                return recomp::RomValidationError::NotYet;
+            }
+            else {
+                return recomp::RomValidationError::IncorrectRom;
+            }
+        }
+    }
+
+    write_file(recomp::get_app_folder_path() / game_entry.stored_filename, rom_data);
+    
+    return recomp::RomValidationError::Good;
 }
 
 extern "C" void osGetMemSize_recomp(uint8_t * rdram, recomp_context * ctx) {
@@ -106,11 +301,6 @@ void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t ar
     func(rdram, &ctx);
 }
 
-void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t dev_address, size_t num_bytes);
-
-std::unique_ptr<uint8_t[]> rom;
-size_t rom_size;
-
 // Recomp generation functions
 extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
 gpr get_entrypoint_address();
@@ -119,9 +309,6 @@ void init_overlays();
 extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size);
 extern "C" void unload_overlays(int32_t ram_addr, uint32_t size);
 
-std::unique_ptr<uint8_t[]> rdram_buffer;
-recomp_context context{};
-
 void read_patch_data(uint8_t* rdram, gpr patch_data_address) {
     const char patches_data_file_path[] = "patches/patches.bin";
     std::ifstream patches_data_file{ patches_data_file_path, std::ios::binary };
@@ -144,28 +331,7 @@ void read_patch_data(uint8_t* rdram, gpr patch_data_address) {
     }
 }
 
-EXPORT extern "C" void init() {
-    {
-        std::ifstream rom_file{ get_rom_name(), std::ios::binary };
-
-        size_t iobuf_size = 0x100000;
-        std::unique_ptr<char[]> iobuf = std::make_unique<char[]>(iobuf_size);
-        rom_file.rdbuf()->pubsetbuf(iobuf.get(), iobuf_size);
-
-        if (!rom_file) {
-            fprintf(stderr, "Failed to open rom: %s\n", get_rom_name());
-            exit(EXIT_FAILURE);
-        }
-
-        rom_file.seekg(0, std::ios::end);
-        rom_size = rom_file.tellg();
-        rom_file.seekg(0, std::ios::beg);
-
-        rom = std::make_unique<uint8_t[]>(rom_size);
-
-        rom_file.read(reinterpret_cast<char*>(rom.get()), rom_size);
-    }
-
+void init(uint8_t* rdram, recomp_context* ctx) {
     // Initialize the overlays
     init_overlays();
 
@@ -175,22 +341,18 @@ EXPORT extern "C" void init() {
     // Load overlays in the first 1MB
     load_overlays(0x1000, (int32_t)entrypoint, 1024 * 1024);
 
-    // Allocate rdram_buffer
-    rdram_buffer = std::make_unique<uint8_t[]>(ultramodern::rdram_size);
-    std::memset(rdram_buffer.get(), 0, ultramodern::rdram_size);
-
     // Initial 1MB DMA (rom address 0x1000 = physical address 0x10001000)
-    do_rom_read(rdram_buffer.get(), entrypoint, 0x10001000, 0x100000);
+    recomp::do_rom_read(rdram, entrypoint, 0x10001000, 0x100000);
 
     // Read in any extra data from patches
-    read_patch_data(rdram_buffer.get(), (gpr)(s32)0x80800100);
+    read_patch_data(rdram, (gpr)(s32)0x80800100);
 
     // Set up stack pointer
-    context.r29 = 0xFFFFFFFF803FFFF0u;
+    ctx->r29 = 0xFFFFFFFF803FFFF0u;
 
     // Set up context floats
-    context.f_odd = &context.f0.u32h;
-    context.mips3_float_mode = false;
+    ctx->f_odd = &ctx->f0.u32h;
+    ctx->mips3_float_mode = false;
 
     // Initialize variables normally set by IPL3
     constexpr int32_t osTvType = 0x80000300;
@@ -201,22 +363,21 @@ EXPORT extern "C" void init() {
     constexpr int32_t osVersion = 0x80000314;
     constexpr int32_t osMemSize = 0x80000318;
     constexpr int32_t osAppNMIBuffer = 0x8000031c;
-    uint8_t *rdram = rdram_buffer.get();
     MEM_W(osTvType, 0) = 1; // NTSC
     MEM_W(osRomBase, 0) = 0xB0000000u; // standard rom base
     MEM_W(osResetType, 0) = 0; // cold reset
     MEM_W(osMemSize, 0) = 8 * 1024 * 1024; // 8MB
 }
 
-std::atomic_int game_started = -1;
+std::atomic<recomp::Game> game_started = recomp::Game::None;
 
-void ultramodern::start_game(int game) {
+void recomp::start_game(recomp::Game game) {
     game_started.store(game);
     game_started.notify_all();
 }
 
 bool ultramodern::is_game_started() {
-    return game_started.load() != -1;
+    return game_started.load() != recomp::Game::None;
 }
 
 void set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks);
@@ -226,24 +387,25 @@ std::atomic_bool exited = false;
 
 void ultramodern::quit() {
     exited.store(true);
-    int desired = -1;
-    game_started.compare_exchange_strong(desired, -2);
+    recomp::Game desired = recomp::Game::None;
+    game_started.compare_exchange_strong(desired, recomp::Game::Quit);
     game_started.notify_all();
 }
 
-void ultramodern::start(WindowHandle window_handle, const audio_callbacks_t& audio_callbacks, const input_callbacks_t& input_callbacks, const gfx_callbacks_t& gfx_callbacks_) {
+void recomp::start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks_) {
+    recomp::check_all_stored_roms();
     set_audio_callbacks(audio_callbacks);
     set_input_callbacks(input_callbacks);
 
-    gfx_callbacks_t gfx_callbacks = gfx_callbacks_;
+    ultramodern::gfx_callbacks_t gfx_callbacks = gfx_callbacks_;
 
-    gfx_callbacks_t::gfx_data_t gfx_data{};
+    ultramodern::gfx_callbacks_t::gfx_data_t gfx_data{};
 
     if (gfx_callbacks.create_gfx) {
         gfx_data = gfx_callbacks.create_gfx();
     }
 
-    if (window_handle == WindowHandle{}) {
+    if (window_handle == ultramodern::WindowHandle{}) {
         if (gfx_callbacks.create_window) {
             window_handle = gfx_callbacks.create_window(gfx_data);
         }
@@ -252,25 +414,34 @@ void ultramodern::start(WindowHandle window_handle, const audio_callbacks_t& aud
         }
     }
 
-    std::thread game_thread{[](ultramodern::WindowHandle window_handle) {
+    // Allocate rdram_buffer
+    std::unique_ptr<uint8_t[]> rdram_buffer = std::make_unique<uint8_t[]>(ultramodern::rdram_size);
+    std::memset(rdram_buffer.get(), 0, ultramodern::rdram_size);
+
+    std::thread game_thread{[](ultramodern::WindowHandle window_handle, uint8_t* rdram) {
         debug_printf("[Recomp] Starting\n");
         
         ultramodern::set_native_thread_name("Game Start Thread");
+        
+        ultramodern::preinit(rdram, window_handle);
 
-        ultramodern::preinit(rdram_buffer.get(), rom.get(), window_handle);
-
-        game_started.wait(-1);
+        game_started.wait(recomp::Game::None);
+        recomp_context context{};
 
         switch (game_started.load()) {
-            case 0:
-                recomp_entrypoint(rdram_buffer.get(), &context);
+            case recomp::Game::MM:
+                if (!recomp::load_stored_rom(recomp::Game::MM)) {
+                    recomp::message_box("Error opening stored ROM! Please restart this program.");
+                }
+                init(rdram, &context);
+                recomp_entrypoint(rdram, &context);
                 break;
-            case -2:
+            case recomp::Game::Quit:
                 break;
         }
         
         debug_printf("[Recomp] Quitting\n");
-    }, window_handle};
+    }, window_handle, rdram_buffer.get()};
 
     while (!exited) {
         using namespace std::chrono_literals;
diff --git a/src/recomp/rt64_layer.cpp b/src/recomp/rt64_layer.cpp
index 047d6a9..f6286ac 100644
--- a/src/recomp/rt64_layer.cpp
+++ b/src/recomp/rt64_layer.cpp
@@ -130,12 +130,13 @@ RT64::UserConfiguration::Antialiasing compute_max_supported_aa(RT64::RenderSampl
     return RT64::UserConfiguration::Antialiasing::None;
 }
 
-RT64::Application* RT64Init(uint8_t* rom, uint8_t* rdram, ultramodern::WindowHandle window_handle, bool debug) {
+RT64::Application* RT64Init(uint8_t* rdram, ultramodern::WindowHandle window_handle, bool debug) {
+    static unsigned char dummy_rom_header[0x40];
     set_rt64_hooks();
 
     GFX_INFO gfx_info{};
 
-    gfx_info.HEADER = rom;
+    gfx_info.HEADER = dummy_rom_header;
     gfx_info.RDRAM = rdram;
     gfx_info.DMEM = DMEM;
     gfx_info.IMEM = IMEM;
diff --git a/src/ui/ui_launcher.cpp b/src/ui/ui_launcher.cpp
index 8787353..ca176ca 100644
--- a/src/ui/ui_launcher.cpp
+++ b/src/ui/ui_launcher.cpp
@@ -1,11 +1,59 @@
 #include "recomp_ui.h"
+#include "recomp_config.h"
+#include "recomp_game.h"
 #include "../../ultramodern/ultramodern.hpp"
 #include "RmlUi/Core.h"
+#include "nfd.h"
+#include <filesystem>
+
+Rml::DataModelHandle model_handle;
+bool mm_rom_valid = false;
+
+void select_rom() {
+	nfdnchar_t* native_path = nullptr;
+	nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
+
+	if (result == NFD_OKAY) {
+		printf("Path: %ls\n", native_path);
+
+		std::filesystem::path path{native_path};
+
+		NFD_FreePathN(native_path);
+		native_path = nullptr;
+
+		recomp::RomValidationError rom_error = recomp::select_rom(path, recomp::Game::MM);
+
+		switch (rom_error) {
+			case recomp::RomValidationError::Good:
+				mm_rom_valid = true;
+				model_handle.DirtyVariable("mm_rom_valid");
+				break;
+			case recomp::RomValidationError::FailedToOpen:
+				recomp::message_box("Failed to open ROM file.");
+				break;
+			case recomp::RomValidationError::NotARom:
+				recomp::message_box("This is not a valid ROM file.");
+				break;
+			case recomp::RomValidationError::IncorrectRom:
+				recomp::message_box("This ROM is not the correct game.");
+				break;
+			case recomp::RomValidationError::NotYet:
+				recomp::message_box("This game isn't supported yet.");
+				break;
+			case recomp::RomValidationError::IncorrectVersion:
+				recomp::message_box("This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
+				break;
+			case recomp::RomValidationError::OtherError:
+				recomp::message_box("An unknown error has occurred.");
+				break;
+		}
+	}
+}
 
 class LauncherMenu : public recomp::MenuController {
 public:
     LauncherMenu() {
-
+		mm_rom_valid = recomp::is_rom_valid(recomp::Game::MM);
     }
 	~LauncherMenu() override {
 
@@ -14,9 +62,20 @@ public:
         return context->LoadDocument("assets/launcher.rml");
 	}
 	void register_events(recomp::UiEventListenerInstancer& listener) override {
+		recomp::register_event(listener, "select_rom",
+			[](const std::string& param, Rml::Event& event) {
+				select_rom();
+			}
+		);
+		recomp::register_event(listener, "rom_selected",
+			[](const std::string& param, Rml::Event& event) {
+				mm_rom_valid = true;
+				model_handle.DirtyVariable("mm_rom_valid");
+			}
+		);
 		recomp::register_event(listener, "start_game",
 			[](const std::string& param, Rml::Event& event) {
-				ultramodern::start_game(0);
+				recomp::start_game(recomp::Game::MM);
 				recomp::set_current_menu(recomp::Menu::None);
 			}
 		);
@@ -38,7 +97,11 @@ public:
 		);
 	}
 	void make_bindings(Rml::Context* context) override {
+		Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
 
+		constructor.Bind("mm_rom_valid", &mm_rom_valid);
+
+		model_handle = constructor.GetModelHandle();
 	}
 };
 
diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp
index 0303054..4e6708b 100644
--- a/src/ui/ui_renderer.cpp
+++ b/src/ui/ui_renderer.cpp
@@ -7,6 +7,7 @@
 
 #include "recomp_ui.h"
 #include "recomp_input.h"
+#include "recomp_game.h"
 
 #include "concurrentqueue.h"
 
@@ -950,8 +951,6 @@ void recomp::get_window_size(int& width, int& height) {
 }
 
 void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
-    printf("RT64 hook init\n");
-
     ui_context = std::make_unique<UIContext>();
 
     ui_context->rml.add_menu(recomp::Menu::Config, recomp::create_config_menu());
@@ -1207,3 +1206,7 @@ void recomp::destroy_ui() {
 recomp::Menu recomp::get_current_menu() {
     return open_menu.load();
 }
+
+void recomp::message_box(const char* msg) {
+    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", msg, nullptr);
+}
diff --git a/ultramodern/events.cpp b/ultramodern/events.cpp
index e6eeadd..3cad0b0 100644
--- a/ultramodern/events.cpp
+++ b/ultramodern/events.cpp
@@ -218,7 +218,7 @@ void run_rsp_microcode(uint8_t* rdram, const OSTask* task, RspUcodeFunc* ucode_f
 }
 
 
-void task_thread_func(uint8_t* rdram, uint8_t* rom, std::atomic_flag* thread_ready) {
+void task_thread_func(uint8_t* rdram, std::atomic_flag* thread_ready) {
     ultramodern::set_native_thread_name("SP Task Thread");
     ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Normal);
 
@@ -284,7 +284,7 @@ uint32_t ultramodern::get_target_framerate(uint32_t original) {
     }
 }
 
-void gfx_thread_func(uint8_t* rdram, uint8_t* rom, std::atomic_flag* thread_ready, ultramodern::WindowHandle window_handle) {
+void gfx_thread_func(uint8_t* rdram, std::atomic_flag* thread_ready, ultramodern::WindowHandle window_handle) {
     bool enabled_instant_present = false;
     using namespace std::chrono_literals;
 
@@ -293,7 +293,7 @@ void gfx_thread_func(uint8_t* rdram, uint8_t* rom, std::atomic_flag* thread_read
 
     ultramodern::GraphicsConfig old_config;
 
-    RT64::Application* application = RT64Init(rom, rdram, window_handle, cur_config.load().developer_mode);
+    RT64::Application* application = RT64Init(rdram, window_handle, cur_config.load().developer_mode);
 
     if (application == nullptr) {
         throw std::runtime_error("Failed to initialize RT64!");
@@ -511,12 +511,12 @@ void ultramodern::send_si_message() {
     osSendMesg(PASS_RDRAM events_context.si.mq, events_context.si.msg, OS_MESG_NOBLOCK);
 }
 
-void ultramodern::init_events(uint8_t* rdram, uint8_t* rom, ultramodern::WindowHandle window_handle) {
+void ultramodern::init_events(uint8_t* rdram, ultramodern::WindowHandle window_handle) {
     std::atomic_flag gfx_thread_ready;
     std::atomic_flag task_thread_ready;
     events_context.rdram = rdram;
-    events_context.sp.gfx_thread = std::thread{ gfx_thread_func, rdram, rom, &gfx_thread_ready, window_handle };
-    events_context.sp.task_thread = std::thread{ task_thread_func, rdram, rom, &task_thread_ready };
+    events_context.sp.gfx_thread = std::thread{ gfx_thread_func, rdram, &gfx_thread_ready, window_handle };
+    events_context.sp.task_thread = std::thread{ task_thread_func, rdram, &task_thread_ready };
     
     // Wait for the two sp threads to be ready before continuing to prevent the game from
     // running before we're able to handle RSP tasks.
diff --git a/ultramodern/ultrainit.cpp b/ultramodern/ultrainit.cpp
index 4d51781..21bbedd 100644
--- a/ultramodern/ultrainit.cpp
+++ b/ultramodern/ultrainit.cpp
@@ -1,9 +1,9 @@
 #include "ultra64.h"
 #include "ultramodern.hpp"
 
-void ultramodern::preinit(uint8_t* rdram, uint8_t* rom, ultramodern::WindowHandle window_handle) {
+void ultramodern::preinit(uint8_t* rdram, ultramodern::WindowHandle window_handle) {
     ultramodern::set_main_thread();
-    ultramodern::init_events(rdram, rom, window_handle);
+    ultramodern::init_events(rdram, window_handle);
     ultramodern::init_timers(rdram);
     ultramodern::init_audio();
     ultramodern::save_init();
diff --git a/ultramodern/ultramodern.hpp b/ultramodern/ultramodern.hpp
index 41b3432..ba04079 100644
--- a/ultramodern/ultramodern.hpp
+++ b/ultramodern/ultramodern.hpp
@@ -51,10 +51,10 @@ constexpr int32_t cart_handle = 0x80800000;
 constexpr int32_t flash_handle = (int32_t)(cart_handle + sizeof(OSPiHandle));
 constexpr uint32_t save_size = 1024 * 1024 / 8; // Maximum save size, 1Mbit for flash
 
-void preinit(uint8_t* rdram, uint8_t* rom, WindowHandle window_handle);
+void preinit(uint8_t* rdram, WindowHandle window_handle);
 void save_init();
 void init_scheduler();
-void init_events(uint8_t* rdram, uint8_t* rom, WindowHandle window_handle);
+void init_events(uint8_t* rdram, WindowHandle window_handle);
 void init_timers(RDRAM_ARG1);
 void set_self_paused(RDRAM_ARG1);
 void yield_self(RDRAM_ARG1);
@@ -129,8 +129,6 @@ struct gfx_callbacks_t {
     create_window_t* create_window;
     update_gfx_t* update_gfx;
 };
-void start(WindowHandle window_handle, const audio_callbacks_t& audio_callbacks, const input_callbacks_t& input_callbacks, const gfx_callbacks_t& gfx_callbacks);
-void start_game(int game);
 bool is_game_started();
 void quit();
 void join_event_threads();