mikage-dev/source/gui-sdl/main.cpp
2024-03-12 17:32:53 +01:00

1016 lines
41 KiB
C++

#include <ui/key_database.hpp>
#include "platform/file_formats/cia.hpp"
#include "platform/file_formats/ncch.hpp"
#include "processes/pxi_fs.hpp"
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
#include "session.hpp"
#include "os.hpp"
#include "framework/logging.hpp"
#include "framework/meta_tools.hpp"
#include "framework/ranges.hpp"
#include "framework/settings.hpp"
#include <framework/profiler.hpp>
#include <SDL.h>
#include <SDL_vulkan.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/null_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <boost/program_options.hpp>
#include <boost/endian/arithmetic.hpp>
#include <range/v3/algorithm/any_of.hpp>
#include <range/v3/algorithm/equal.hpp>
#include <range/v3/algorithm/none_of.hpp>
#include <chrono>
#include <codecvt>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <numeric>
#include <thread>
#include <vulkan/vulkan.hpp>
void InstallCIA(std::filesystem::path, spdlog::logger&, const KeyDatabase&, HLE::PXI::FS::FileContext&, HLE::PXI::FS::File&);
using boost::endian::big_uint32_t;
static const char* app_name = "Mikage";
#include "sdl_vulkan_display.hpp"
std::mutex g_vulkan_queue_mutex; // TODO: Turn into a proper interface
namespace bpo = boost::program_options;
static std::vector<std::pair<int16_t, int16_t>> g_samples;
static std::mutex g_sample_mutex;
void TeakraAudioCallback(std::array<int16_t, 2> samples) {
std::unique_lock lock(g_sample_mutex);
// g_samples.push_back(std::pair{samples[0], samples[1]});
}
#ifdef __ANDROID__ // TODO: Consider enabling this elsewhere, too!
PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXTptr;
PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXTptr;
extern "C" {
VkResult vkCreateDebugReportCallbackEXT(VkInstance_T* a, VkDebugReportCallbackCreateInfoEXT const* b, VkAllocationCallbacks const* c, VkDebugReportCallbackEXT_T** d) {
return vkCreateDebugReportCallbackEXTptr(a, b, c, d);
}
void vkDestroyDebugReportCallbackEXT(VkInstance_T* a, VkDebugReportCallbackEXT_T* b, VkAllocationCallbacks const* c) {
vkDestroyDebugReportCallbackEXTptr(a, b, c);
}
}
static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(
VkDebugReportFlagsEXT msgFlags,
VkDebugReportObjectTypeEXT objType,
uint64_t srcObject, size_t location,
int32_t msgCode, const char * pLayerPrefix,
const char * pMsg, void * pUserData )
{
if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
__android_log_print(ANDROID_LOG_ERROR,
"MikageValidation",
"ERROR: [%s] Code %i : %s",
pLayerPrefix, msgCode, pMsg);
} else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
__android_log_print(ANDROID_LOG_WARN,
"MikageValidation",
"WARNING: [%s] Code %i : %s",
pLayerPrefix, msgCode, pMsg);
} else if (msgFlags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) {
__android_log_print(ANDROID_LOG_WARN,
"MikageValidation",
"PERFORMANCE WARNING: [%s] Code %i : %s",
pLayerPrefix, msgCode, pMsg);
} else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
__android_log_print(ANDROID_LOG_INFO,
"MikageValidation", "INFO: [%s] Code %i : %s",
pLayerPrefix, msgCode, pMsg);
} else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
__android_log_print(ANDROID_LOG_VERBOSE,
"MikageValidation", "DEBUG: [%s] Code %i : %s",
pLayerPrefix, msgCode, pMsg);
}
// Returning false tells the layer not to stop when the event occurs, so
// they see the same behavior with and without validation layers enabled.
return VK_FALSE;
}
#endif
static volatile int wait_debugger = 0;
namespace Settings {
inline std::istream& operator>>(std::istream& is, CPUEngine& engine) {
std::string str;
is >> str;
if (str == "narmive") {
engine = CPUEngine::NARMive;
} else {
is.setstate(std::ios_base::failbit);
}
return is;
}
inline std::ostream& operator<<(std::ostream& os, CPUEngine engine) {
switch (engine) {
case CPUEngine::NARMive:
return (os << "narmive");
}
}
inline std::istream& operator>>(std::istream& is, ShaderEngine& engine) {
std::string str;
is >> str;
if (str == "interpreter") {
engine = ShaderEngine::Interpreter;
} else if (str == "bytecode") {
engine = ShaderEngine::Bytecode;
} else if (str == "glsl") {
engine = ShaderEngine::GLSL;
} else {
is.setstate(std::ios_base::failbit);
}
return is;
}
inline std::ostream& operator<<(std::ostream& os, ShaderEngine engine) {
switch (engine) {
case ShaderEngine::Interpreter:
return (os << "interpreter");
case ShaderEngine::Bytecode:
return (os << "bytecode");
case ShaderEngine::GLSL:
return (os << "glsl");
}
}
}
namespace CustomEvents {
enum {
CirclePadPosition = SDL_USEREVENT
};
}
int main(int argc, char* argv[]) {
while (wait_debugger) {
}
Settings::Settings settings;
bool enable_logging;
bool bootstrap_nand;
{
std::string filename;
bool enable_debugging;
bpo::options_description desc("Allowed options");
desc.add_options()
("help", "produce help message")
("input", bpo::value<std::string>(&filename), "Input file to load")
("launch_menu", bpo::bool_switch(), "Launch Home Menu from NAND on startup")
("debug", bpo::bool_switch(&enable_debugging), "Connect to GDB upon startup")
("dump_frames", bpo::bool_switch(), "Dump frame data")
// TODO: Instead read this from the game image... or add support for software reboots!
("appmemtype", bpo::value<unsigned>()->default_value(0), "APPMEMTYPE for configuration memory")
("attach_to_process", bpo::value<unsigned>(), "Process to attach the debugger (if enabled) to during startup")
("render_on_cpu", bpo::bool_switch(), "Render 3D graphics in software")
("shader_engine", bpo::value<Settings::ShaderEngine>()->default_value(Settings::ShaderEngineTag::default_value()), "Select which Shader engine to use (interpreter, bytecode, or glsl)")
("enable_logging", bpo::bool_switch(&enable_logging), "Enable logging (slow!)")
("bootstrap_nand", bpo::bool_switch(&bootstrap_nand), "Bootstrap NAND from game update partition")
("enable_audio", bpo::bool_switch(), "Enable audio emulation (slow!)")
;
boost::program_options::positional_options_description p;
p.add("input", -1);
bpo::variables_map vm;
try {
bpo::store(bpo::command_line_parser(argc, argv).options(desc).positional(p).run(), vm);
if (!vm.count("input") && !vm["launch_menu"].as<bool>())
throw bpo::required_option("input or launch_menu"); // TODO: Better string?
if (vm["debug"].as<bool>() || vm.count("attach_to_process")) {
if (!vm["debug"].as<bool>() || !vm.count("attach_to_process")) {
throw std::runtime_error("Cannot use either of --debug or --attach_to_process without specifying the other");
}
}
bpo::notify(vm);
} catch (bpo::required_option& e) {
std::cerr << "ERROR: " << e.what() << std::endl;
return 1;
} catch (bpo::invalid_command_line_syntax& e) {
std::cerr << "ERROR, invalid command line syntax: " << e.what() << std::endl;
return 1;
} catch (bpo::multiple_occurrences& e) {
std::cerr << "ERROR: " << e.what() << std::endl;
return 1;
}
if (vm.count("help")) {
std::cout << desc << std::endl;
return 0;
}
settings.set<Settings::DumpFrames>(vm["dump_frames"].as<bool>());
settings.set<Settings::BootToHomeMenu>(vm["launch_menu"].as<bool>());
if (enable_debugging) {
settings.set<Settings::ConnectToDebugger>(true);
settings.set<Settings::AttachToProcessOnStartup>(vm["attach_to_process"].as<unsigned>());
}
settings.set<Settings::AppMemType>(vm["appmemtype"].as<unsigned>());
settings.set<Settings::CPUEngineTag>(Settings::CPUEngine::NARMive);
if (vm["render_on_cpu"].as<bool>()) {
settings.set<Settings::RendererTag>(Settings::Renderer::Software);
if (!vm.count("shader_engine")) {
// Switch default from GLSL to the bytecode processor
settings.set<Settings::ShaderEngineTag>(Settings::ShaderEngine::Bytecode);
}
}
settings.set<Settings::ShaderEngineTag>(vm["shader_engine"].as<Settings::ShaderEngine>());
if (settings.get<Settings::RendererTag>() == Settings::Renderer::Software && settings.get<Settings::ShaderEngineTag>() == Settings::ShaderEngine::GLSL) {
std::cerr << "ERROR: Cannot use GLSL shader engine with software rendering" << std::endl;
std::exit(1);
}
settings.set<Settings::EnableAudioEmulation>(vm["enable_audio"].as<bool>());
if (vm.count("input")) {
Settings::InitialApplicationTag::HostFile file{vm["input"].as<std::string>()};
settings.set<Settings::InitialApplicationTag>({file});
}
}
auto log_manager = Meta::invoke([enable_logging]() {
spdlog::sink_ptr logging_sink;
if (!enable_logging) {
logging_sink = std::make_shared<spdlog::sinks::null_sink_mt>();
} else {
logging_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
}
return std::make_unique<LogManager>(logging_sink);
});
auto frontend_logger = log_manager->RegisterLogger("FRONTEND");
auto keydb = LoadKeyDatabase(*frontend_logger, "./aes_keys.txt");
if (bootstrap_nand) // Experimental system bootstrapper
try {
// TODO: Replicate the functionality of ns:s's CardUpdateInitialize on boot:
// Compare the title version of CVer in emulated NAND against the title
// version in the TMD of the CVer CIA in the game update partition.
auto gamecard = LoadGameCard(*frontend_logger, settings);
auto update_partition = gamecard->GetPartitionFromId(Loader::NCSDPartitionId::UpdateData);
if (!update_partition) {
throw std::runtime_error("Couldn't find update partition");
}
auto file_context = HLE::PXI::FS::FileContext { *frontend_logger };
auto [result] = (*update_partition)->Open(file_context, false);
if (result != HLE::OS::RESULT_OK) {
throw std::runtime_error("Failed to open update partition");
}
// Open RomFS for update partition
auto romfs = HLE::PXI::FS::NCCHOpenExeFSSection(*frontend_logger, file_context, keydb, std::move(*update_partition), 0, {});
FileFormat::RomFSLevel3Header level3_header;
const uint32_t lv3_offset = 0x1000;
std::tie(result) = romfs->Open(file_context, false);
uint32_t bytes_read;
std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset, sizeof(level3_header), HLE::PXI::FS::FileBufferInHostMemory(level3_header));
if (result != HLE::OS::RESULT_OK) {
throw std::runtime_error("Failed to read update partition RomFS");
}
for (uint32_t metadata_offset = 0; metadata_offset < level3_header.file_metadata_size;) {
FileFormat::RomFSFileMetadata file_metadata;
std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset + level3_header.file_metadata_offset + metadata_offset, sizeof(file_metadata), HLE::PXI::FS::FileBufferInHostMemory(file_metadata));
if (result != HLE::OS::RESULT_OK) {
throw std::runtime_error("Failed to read file metadata");
}
metadata_offset += sizeof(file_metadata);
std::u16string filename;
filename.resize((file_metadata.name_size + 1) / 2);
std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset + level3_header.file_metadata_offset + metadata_offset, file_metadata.name_size, HLE::PXI::FS::FileBufferInHostMemory(filename.data(), file_metadata.name_size));
if (result != HLE::OS::RESULT_OK) {
throw std::runtime_error("Failed to read filename from metadata");
}
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> conversion;
std::string filename2 { conversion.to_bytes(filename) };
fprintf(stderr, "FOUND FILENAME: %s\n", filename2.c_str());
if (filename2.ends_with(".cia")) {
auto cia_file = std::make_unique<HLE::PXI::FS::FileView>(std::move(romfs), lv3_offset + level3_header.file_data_offset + file_metadata.data_offset, file_metadata.data_size);
FileFormat::CIAHeader cia_header;
std::tie(result, bytes_read) = cia_file->Read(file_context, 0, sizeof(cia_header), HLE::PXI::FS::FileBufferInHostMemory(cia_header));
if (result != HLE::OS::RESULT_OK) {
throw std::runtime_error("Failed to read file data");
}
fprintf(stderr, "CIA header size: %#x\n", cia_header.header_size);
std::filesystem::path content_dir = "./data";
InstallCIA(std::move(content_dir), *frontend_logger, keydb, file_context, *cia_file);
romfs = cia_file->ReleaseParentAndClose();
}
// Align filename size to 4 bytes
metadata_offset += (file_metadata.name_size + 3) & ~3;
}
// Initial system setup requires title 0004001000022400 to be present.
// Normally, this is the Camera app, which isn't bundled in game update
// partitions. Since the setup only reads the ExeFS icon that aren't
// specific to the Camera app (likely to perform general checks), we
// use the Download Play app as a drop-in replacement if needed.
if (!std::filesystem::exists("./data/00040010/00022400")) {
std::filesystem::copy("./data/00040010/00022100", "./data/00040010/00022400", std::filesystem::copy_options::recursive);
}
std::filesystem::create_directories("./data/ro/sys");
std::filesystem::create_directories("./data/rw/sys");
std::filesystem::create_directories("./data/twlp");
// Create dummy HWCAL0, required for cfg module to work without running initial system setup first
char zero[128]{};
{
std::ofstream hwcal0("./data/ro/sys/HWCAL0.dat");
const auto size = 2512;
for (unsigned i = 0; i < size / sizeof(zero); ++i) {
hwcal0.write(zero, sizeof(zero));
}
hwcal0.write(zero, size % sizeof(zero));
}
// Set up dummy rw/sys/SecureInfo_A with region EU
// TODO: Let the user select the region
{
std::ofstream info("./data/rw/sys/SecureInfo_A");
info.write(zero, sizeof(zero));
info.write(zero, sizeof(zero));
char region = 2; // Europe
info.write(&region, 1);
info.write(zero, 0x10);
}
// TODO: Set up shared font
// TODO: Set up GPIO
} catch (std::runtime_error&) {
throw;
}
// Initialize SDL
// const unsigned window_width = 800, window_height = 960;
// const unsigned window_width = 720, window_height = 864;
const unsigned window_width = 400, window_height = 480;
// const unsigned window_width = 800, window_height = 480;
// TODO: Fix indentation... but we do want this scope so that all cleanup is finished by the time we print the final log message
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
frontend_logger->critical("Failed to initialize SDL: {}", SDL_GetError());
return 1;
}
#ifdef TRACY_ENABLE
// SDL's "SDLAudioP2" thread creates unintelligible back traces in profiles for some reason
#define DISABLE_AUDIO
#error "TEST"
#endif
#define DISABLE_AUDIO // TODO: Drop.
#ifndef DISABLE_AUDIO
if (SDL_Init(SDL_INIT_AUDIO) != 0) {
frontend_logger->critical("Failed s to initialize SDL: {}", SDL_GetError());
return 1;
}
#endif
//exit(1);
g_samples.reserve(32768);
#ifndef DISABLE_AUDIO
static std::ofstream ofs("samples.raw", std::ios_base::binary);
static std::ofstream ofs2("samples2.raw", std::ios_base::binary);
static std::vector<uint16_t> data;
static std::vector<uint16_t> data2;
SDL_AudioCallback abc = [](void*, uint8_t* stream, int num_bytes) {
static uint32_t index = 0;
std::unique_lock lock(g_sample_mutex);
if (g_samples.empty()) {
g_samples.push_back(std::pair<int16_t, int16_t>(0, 0));
}
// TODO: Also handle the emulator running too fast!
auto num_ready_samples = std::min<uint32_t>(g_samples.size(), num_bytes / 4);
//fprintf(stderr, "Copying %d/%d samples\n", num_ready_samples, num_bytes / 4);
if (num_ready_samples > 1) {
g_samples.erase(g_samples.begin());
--num_ready_samples;
}
for (uint32_t sample = 0; sample < num_bytes / 4; ++sample) {
auto& data = g_samples[sample * num_ready_samples / (num_bytes / 4)];
memcpy(stream + sample * 4, &data, sizeof(data));
index++;
}
// for (uint32_t sample = 0; sample < num_ready_samples; ++sample) {
// auto& data = g_samples[sample];
// ofs.write((char*)&data, sizeof(data) / 2);
// static int index =0;
// index++;
// if ((index % 1000) == 0) {
// ofs.flush();
// }
// }
// Always leave at least 1 sample for interpolation
g_samples.erase(g_samples.begin(), g_samples.begin() + (num_ready_samples - 1));
// for (uint32_t sample = 0; sample < (unsigned)num_bytes / 4; ++sample) {
// int16_t data = sinf(440.f * 6.28 * index / 32728.f/* 44100.f*/) * 16384.f * (0.1f + (1.0 + sinf(index / 100000.f)) / 2);
// memcpy(stream + sample * 4, &data, sizeof(data));
// data = sinf(440.f * 6.28 * index / 32728.f/* 44100.f*/) * 16384.f * (1.1f - (1.0 + sinf(index / 100000.f)) / 2);
// memcpy(stream + sample * 4 + sizeof(data), &data, sizeof(data));
// ++index;
// }
};
SDL_AudioSpec desired_audio_spec {
.freq = 32728,
.format = AUDIO_S16LSB,
.channels = 2,
.silence = 0, // filled by SDL
.samples = 4096, // TODO?
.padding = 0,
.size = 0, // filled by SDL
// NOTE: Consider using SDL_QueueAudio as an alternative
.callback = abc,
.userdata = nullptr
} ;
SDL_AudioSpec audio_spec;
// SDL_AUDIO_DRIVER_DISK
//SDL_AUDIO_DRIVER_DISK;
fmt::print(stderr, "Opening SDL audio device\n");
auto device_id = SDL_OpenAudioDevice(nullptr, false, &desired_audio_spec, &audio_spec, 0 /* no allowed changes; let SDL auto-convert */);
if (device_id == 0) {
throw std::runtime_error("Failed to open SDL audio device");
}
fmt::print(stderr, "Opened SDL audio device with {} samples\n", audio_spec.freq);
// exit(1);
// TODO: Setup audio callback
SDL_PauseAudioDevice(device_id, false);
#endif
// TODO: For some reason, SDL picks RGB565 as the surface format for us... these hints don't help, either :(
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
const bool enable_fullscreen = false;
// const bool enable_fullscreen = true;
auto sdl_window_flags = enable_fullscreen ? (SDL_WINDOW_VULKAN | SDL_WINDOW_FULLSCREEN_DESKTOP) : SDL_WINDOW_VULKAN;
std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> window(SDL_CreateWindow(app_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, sdl_window_flags), SDL_DestroyWindow);
if (!window) {
throw std::runtime_error("Failed to create window");
}
int actual_window_width, actual_window_height;
SDL_GetWindowSize(&*window, &actual_window_width, &actual_window_height);
std::array<Layout, SDLVulkanDisplay::FrameData::num_screen_ids> layouts;
if (enable_fullscreen) {
// Stretch top screen image by an integral factor; hide the bottom screen
// TODO: Allow for unhiding the bottom screen!
unsigned factor = std::min(actual_window_width / 400, actual_window_height / 480);
unsigned usable_window_width = factor * 400;
unsigned usable_window_height = factor * 240;
// NOTE: For recording, make sure the screen positions are aligned by 16
layouts = {{
// { true, 0/*(actual_window_width - usable_window_width) / 2*/, 0, static_cast<uint32_t>(usable_window_width), static_cast<uint32_t>(usable_window_width * 240 / 400) },
// { false, 0, 0, 0, 0 },
//// { false, 0, 0, 0, 0 },
// { true, static_cast<uint32_t>(usable_window_width) * 40 / 400, (actual_window_height - usable_window_height), static_cast<uint32_t>(usable_window_width) * 320 / 400, static_cast<uint32_t>(usable_window_width * 240 / 400) },
{ true, 28, /*40*/ 180, 400 * 3, 240 * 3 },
{ false, 0, 0, 0, 0 },
// { false, 0, 0, 0, 0 },
// { true, 1920 - 640 - 35 + 3, 1080 - 300 - 480 + 4, 640, 480 }, // vertically-centered bottom screen
{ true, 1920 - 640 - 28 + 3, 180, 640, 480 }, // top-aligned bottom screen
}};
} else {
unsigned usable_window_width = actual_window_width;
unsigned usable_window_height = actual_window_height;
unsigned bottom_width = usable_window_width * 320 / 400;
unsigned bottom_height = bottom_width * 240 / 320;
if (bottom_height * 2 > (unsigned)usable_window_height) {
bottom_height = usable_window_height / 2;
bottom_width = bottom_height * 320 / 240;
usable_window_width = bottom_width * 400 / 320;
}
layouts = {{
{ true, (actual_window_width - usable_window_width) / 2, 0, static_cast<uint32_t>(usable_window_width), static_cast<uint32_t>(bottom_width * 240 / 320) },
{ false, 0, 0, 0, 0 },
{ true, (actual_window_width - bottom_width) / 2, static_cast<uint32_t>(bottom_width * 240 / 320), static_cast<uint32_t>(bottom_width), static_cast<uint32_t>(bottom_width * 240 / 320) },
// { true, 0, 0, 400, 240 },
// { false, 0, 0, 0, 0 },
// { true, 400, 120, 320, 240 },
}};
}
if (settings.get<Settings::DumpFrames>()) {
throw std::runtime_error("Frame dumping not implemented");
}
auto display = std::make_unique<SDLVulkanDisplay>(frontend_logger, *window, layouts);
#ifdef __ANDROID__
//#if 0 // TODO: Re-enable!
{
auto instance = *display->instance;
vkCreateDebugReportCallbackEXTptr = (PFN_vkCreateDebugReportCallbackEXT)
vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
vkDestroyDebugReportCallbackEXTptr = (PFN_vkDestroyDebugReportCallbackEXT)
vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");
}
vk::DebugReportCallbackCreateInfoEXT callback_info { ~vk::DebugReportFlagsEXT{}, DebugReportCallback };
auto cb = display->instance->createDebugReportCallbackEXTUnique(callback_info);
#endif
std::thread emuthread;
std::exception_ptr emuthread_exception = nullptr;
std::unique_ptr<EmuSession> session;
try {
for (auto key_index : ranges::views::indexes(keydb.aes_slots)) {
auto& aes_slot = keydb.aes_slots[key_index];
for (auto key_type : { 'X', 'Y', 'N' }) {
auto& key = key_type == 'X' ? aes_slot.x
: key_type == 'Y' ? aes_slot.y
: aes_slot.n;
if (key.has_value()) {
frontend_logger->info("Parsed key{} for slot {:#4x}", key_type, key_index);
}
}
}
for (auto key_index : ranges::views::indexes(keydb.common_y)) {
auto& key = keydb.common_y[key_index];
if (key.has_value()) {
frontend_logger->info("Parsed common{}", key_index);
}
}
auto gamecard = LoadGameCard(*frontend_logger, settings);
session = std::make_unique<EmuSession>(*log_manager, settings, *display, *display, keydb, std::move(gamecard));
emuthread = std::thread {
[&emuthread_exception, &session]() {
try {
session->Run();
} catch (...) {
emuthread_exception = std::current_exception();
}
}
};
bool background = false;
auto& input = session->input;
auto& circle_pad = session->circle_pad;
// std::array<bool, 3> active_images { };
// std::array<bool, 3> active_images { true, false, true };
std::array<bool, 3> active_images { true, true, true };
// Application loop
for (;;) {
// Process event queue
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
frontend_logger->error("SDL_WINDOWEVENT_EXPOSED");
// SDL_RenderClear(renderer.get());
// SDL_RenderPresent(renderer.get());
break;
case SDL_WINDOWEVENT_CLOSE:
frontend_logger->error("SDL_WINDOWEVENT_CLOSE");
goto quit_application_loop;
case SDL_WINDOWEVENT_RESIZED:
frontend_logger->error("SDL_WINDOWEVENT_RESIZED");
break;
default:
break;
}
break;
case SDL_QUIT:
frontend_logger->error("SDL_QUIT");
goto quit_application_loop;
case SDL_APP_WILLENTERBACKGROUND:
frontend_logger->error("About to enter background, destroying swapchain");
display->ResetSwapchainResources();
background = true;
break;
case SDL_APP_DIDENTERFOREGROUND:
frontend_logger->error("Entered foreground, recreating swapchain");
display->CreateSwapchain();
background = false;
break;
case SDL_MOUSEBUTTONDOWN:
{
if (event.button.button != SDL_BUTTON_LEFT) {
break;
}
float x = (event.button.x - layouts[2].x) / static_cast<float>(layouts[2].width);
float y = (event.button.y - layouts[2].y) / static_cast<float>(layouts[2].height);
if (x >= 0.f && x <= 1.f && y >= 0.f && y <= 1.f) {
input.SetTouch(x, y);
}
break;
}
// NOTE: SDL provides a separate event type for touch events, but
// (at least on Android) it dispatches both touch events and
// mouse events upon touch input. Hence, we only need to
// handle mouse events
case SDL_MOUSEMOTION:
{
if ((event.motion.state & SDL_BUTTON_LMASK) == 0) {
break;
}
float x = (event.button.x - layouts[2].x) / static_cast<float>(layouts[2].width);
float y = (event.button.y - layouts[2].y) / static_cast<float>(layouts[2].height);
if (x >= 0.f && x <= 1.f && y >= 0.f && y <= 1.f) {
input.SetTouch(x, y);
} else {
// NOTE: If the user keeps holding the mouse button when
// moving the mouse outside the emulated screen area,
// they presumably do not intend to stop the touch
// gesture, yet
}
break;
}
case SDL_MOUSEBUTTONUP:
if (event.button.button != SDL_BUTTON_LEFT) {
break;
}
input.EndTouch();
break;
// TODO: Need a way for the emulator to acknowledge key presses!
// Otherwise, we might process a KEYDOWN event followed by
// KEYUP before the emulator core provides the next content
// frame
case SDL_KEYDOWN:
case SDL_KEYUP:
{
// Ignore repeats
if (event.key.repeat) {
// TODO: Does this actually occur in practice?
break;
}
bool pressed = (event.type == SDL_KEYDOWN);
// TODO: For the default key mappings, use scancodes (physical key positions in QWERTY layout) and map them to key codes (the key actually pressed as seen by the user)
switch (event.key.keysym.sym) {
case SDLK_LEFT:
input.SetPressedY(pressed);
break;
case SDLK_UP:
input.SetPressedX(pressed);
break;
case SDLK_DOWN:
input.SetPressedB(pressed);
break;
case SDLK_RIGHT:
input.SetPressedA(pressed);
break;
case SDLK_BACKSPACE:
input.SetPressedHome(pressed);
break;
case SDLK_q:
case SDLK_VOLUMEUP:
input.SetPressedL(pressed);
break;
case SDLK_e:
case SDLK_VOLUMEDOWN:
input.SetPressedR(pressed);
break;
case SDLK_RETURN:
input.SetPressedStart(pressed);
break;
case SDLK_a:
circle_pad.first -= 1.0f * (pressed ? 1.0 : -1.0);
input.SetCirclePad(circle_pad.first, circle_pad.second);
break;
case SDLK_d:
circle_pad.first += 1.0f * (pressed ? 1.0 : -1.0);
input.SetCirclePad(circle_pad.first, circle_pad.second);
break;
case SDLK_w:
circle_pad.second += 1.0f * (pressed ? 1.0 : -1.0);
input.SetCirclePad(circle_pad.first, circle_pad.second);
break;
case SDLK_s:
circle_pad.second -= 1.0f * (pressed ? 1.0 : -1.0);
input.SetCirclePad(circle_pad.first, circle_pad.second);
break;
case SDLK_j:
input.SetPressedDigiLeft(pressed);
break;
case SDLK_l:
input.SetPressedDigiRight(pressed);
break;
case SDLK_i:
input.SetPressedDigiUp(pressed);
break;
case SDLK_k:
input.SetPressedDigiDown(pressed);
break;
case SDLK_7:
if (settings.get<Settings::RendererTag>() == Settings::Renderer::Vulkan) {
settings.set<Settings::RendererTag>(Settings::Renderer::Software);
} else {
settings.set<Settings::RendererTag>(Settings::Renderer::Vulkan);
}
break;
case SDLK_AC_BACK:
if (event.type == SDL_KEYDOWN) {
goto quit_application_loop;
}
break;
}
break;
}
case CustomEvents::CirclePadPosition:
{
float x, y;
memcpy(&x, &event.user.data1, sizeof(x));
memcpy(&y, reinterpret_cast<char*>(&event.user.data1) + sizeof(x), sizeof(y));
input.SetCirclePad(x, y);
break;
}
default:
break;
}
}
if (background) {
continue;
}
// Process next frame from the emulation core, if any (and do so three times for each screen id... TODO: Find a nicer way of doing this)
display->BeginFrame();
// Repeat until at least one image is active
static int loll = 0;
loll++;
// TODO: Proper time keeping
display->SeekTo(loll * 3);
// TODO: If recording via renderdoc, wait for all active images to arrive
//std::this_thread::sleep_for(std::chrono::milliseconds { 20 });
//std::this_thread::sleep_for(std::chrono::milliseconds { 30 });
for (auto stream_id : { EmuDisplay::DataStreamId::TopScreenLeftEye, EmuDisplay::DataStreamId::TopScreenRightEye, EmuDisplay::DataStreamId::BottomScreen }) {
// TODO: If capturing with renderdoc, sync presentation to rendering
// while (stream_id == EmuDisplay::DataStreamId::TopScreenLeftEye && display->ready_frames[Meta::to_underlying(stream_id)].empty()) {
// display->SeekTo(loll * 3);
// std::this_thread::yield();
// }
// TODO: Add EmuDisplay interface
if (display->ready_frames[Meta::to_underlying(stream_id)].empty()) {
continue;
}
auto& frame = display->GetCurrent(stream_id);
// fprintf(stderr, "Processing frame %lu\n", frame.timestamp);
display->ProcessDataSource(frame, stream_id);
}
#if 0 // TODO: Port entire logic over
auto begin_frame_time = std::chrono::steady_clock::now();
for (std::array<bool, 3> has_new_image { }; true;) {
#ifdef RENDERDOC_WORKAROUND
if (!has_new_image[0] && !has_new_image[2]) {
dumb_lock = true;
}
#endif
if (!has_new_image[0]) {
has_new_image[0] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::TopScreenLeftEye);
}
if (!has_new_image[1]) {
has_new_image[1] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::TopScreenRightEye);
}
if (!has_new_image[2]) {
has_new_image[2] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::BottomScreen);
}
// If any image has been submitted, wait until at least one top screen and one bottom screen image have arrived. Otherwise, we'd artificially limit ourselves to 30 fps (on a 60 Hz display) by pulling the two images in separate frames since since the images don't arrive simultaneously
if (emuthread_exception) {
break;
}
// Infer screen activity information from MMIO instead
using namespace std::chrono_literals;
// Check if any new images have become active
if (!ranges::equal(has_new_image, active_images, std::less_equal{})) {
for (unsigned i = 0; i < std::size(active_images); ++i) {
active_images[i] |= has_new_image[i];
}
if (ranges::find(active_images, false) != std::end(active_images)) {
// Wait a bit in case others become active too so that we
// avoid displaying the images in separate host frames.
std::this_thread::sleep_for(3ms);
continue;
} else {
break;
}
}
// Wait until *any* image is active
if (ranges::find(active_images, true) == ranges::end(active_images)) {
std::this_thread::yield();
continue;
}
// Wait until all images have been submitted that have been found to be active
if (has_new_image[0] == active_images[0] && has_new_image[1] == active_images[1] && has_new_image[2] == active_images[2]) {
break;
}
// Move frames to "inactive" state if no new image is received for more than one host frame interval
if (std::chrono::steady_clock::now() - begin_frame_time > 17ms) {
for (unsigned i = 0; i < std::size(active_images); ++i) {
active_images[i] &= has_new_image[i];
}
break;
}
std::this_thread::yield();
}
#endif
{
std::unique_lock lock(g_vulkan_queue_mutex);
display->EndFrame();
}
#ifdef RENDERDOC_WORKAROUND
dumb_lock = true;
#endif
auto metrics = session->profiler.Freeze();
auto print_metrics = [&frontend_logger](std::string indent, Profiler::FrozenMetrics& frozen, auto& cont) -> void {
frontend_logger->info("{}{}: {} ms", indent, frozen.activity.GetName(), frozen.metric.total.count() / 1000.f);
indent += " ";
auto misc_activity = std::accumulate(frozen.sub_metrics.begin(), frozen.sub_metrics.end(), frozen.metric.total,
[](auto& dur, auto& frozen) { return dur - frozen.metric.total; });
if (misc_activity.count() < 0) {
// TODO
// throw std::runtime_error("Negative misc activity?");
}
// Don't print misc activity if there are any sub activities to begin with, or if the relative difference is negligible
if (!frozen.sub_metrics.empty() && misc_activity >= frozen.metric.total * 0.03 ) {
frontend_logger->info("{}Misc: {} ms", indent, misc_activity.count() / 1000.f);
}
for (auto& sub_activity : frozen.sub_metrics) {
cont(indent, sub_activity, cont);
}
};
// frontend_logger->info("PROFILE:");
// print_metrics("", metrics, print_metrics);
// TODO: Have the emu thread post an SDL event about this instead of checking every frame
if (emuthread_exception) {
std::rethrow_exception(emuthread_exception);
}
}
} catch (std::exception& err) {
// TODO: On Android, the "back" button doesn't do anything while this is displayed
frontend_logger->error(err.what());
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Emulation error", err.what(), nullptr);
goto quit_application_loop;
} catch (...) {
auto message = "Unhandled exception!\n";
frontend_logger->error(message);
// TODO: On Android, the "back" button doesn't do anything while this is displayed
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Emulation error", "Unknown exception", nullptr);
goto quit_application_loop;
}
quit_application_loop:
frontend_logger->info("Initiating emulator shutdown...");
if (session) {
display->exit_requested = true;
if (session->setup->os) {
session->setup->os->RequestStop();
}
// Wait for emulation thread (OS) to shut down.
// This may be in one of these blocking states:
// * OS scheduler stuck in an infinite idle loop
// * An internal flag set by RequestStop() is checked here
// * Attempting to push frame data when no queue slot is available
// * Display::exit_requested is checked here
// * CPU emulation stuck in a loop (e.g. due to the emulated app waiting on external events using a spinlock)
// * This is handled indirectly by regular preemption
frontend_logger->info("Waiting for emulation thread...");
emuthread.join();
// Wait for the host GPU to catch up now that no more work will be pushed
frontend_logger->info("Waiting for host GPU to be idle...");
display->graphics_queue.waitIdle();
display->present_queue.waitIdle();
display->device->waitIdle();
frontend_logger->info("Shutting down DSP JIT...");
session.reset();
}
frontend_logger->info("Clearing display resources...");
display->ClearResources();
frontend_logger->info("Clearing remaining resources...");
}
frontend_logger->info("... done. Have a nice day!");
// NOTE: This function isn't actually the true main(), since SDL overrides
// it. Hence we must explicitly return 0 here (whereas usually 0 is
// implicitly returned from main)
return 0;
}