/*
Copyright 2025 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see .
*/
#include "settings.h"
#include "gui.h"
#include "IconsFontAwesome6.h"
#include "mainui.h"
#include "log/LogManager.h"
#include "hw/maple/maple_if.h"
#include "imgui_stdlib.h"
#include "input/dreampotato.h"
#ifdef GDB_SERVER
#include "hw/mem/addrspace.h"
#endif
static void gui_settings_advanced()
{
#if FEAT_SHREC != DYNAREC_NONE
header(T("CPU Mode"));
{
ImGui::Columns(2, "cpu_modes", false);
OptionRadioButton(T("Dynarec"), config::DynarecEnabled, true,
T("Use the dynamic recompiler. Recommended in most cases"));
ImGui::NextColumn();
OptionRadioButton(T("Interpreter"), config::DynarecEnabled, false,
T("Use the interpreter. Very slow but may help in case of a dynarec problem"));
ImGui::Columns(1, nullptr, false);
OptionSlider(T("SH4 Clock"), config::Sh4Clock, 100, 300,
T("Over/Underclock the main SH4 CPU. Default is 200 MHz. Other values may crash, freeze or trigger unexpected nuclear reactions."),
"%d MHz");
}
#ifdef GDB_SERVER
ImGui::Spacing();
header("Virtual memory addresses");
{
void *ram_base, *ram, *vram, *aram;
addrspace::getAddress(&ram_base, &ram, &vram, &aram);
ImGui::Text("Base Address: %p", ram_base);
if (ram == nullptr) {
const ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f);
ImGui::TextColored(gray, "%s", "RAM adresses are not available until the emulation is started");
} else {
ImGui::Columns(3, "virtualMemoryAddress", false);
ImGui::Text("RAM: %p", ram);
ImGui::NextColumn();
ImGui::Text("VRAM64: %p", vram);
ImGui::NextColumn();
ImGui::Text("ARAM: %p", aram);
ImGui::Columns(1, nullptr, false);
}
}
ImGui::Spacing();
header("Debugging");
{
OptionCheckbox("Enable GDB", config::GDB, "GDB debugging support, disables Dynarec and dramatically reduces performance when a debugger is connected.");
OptionCheckbox("Wait for connection", config::GDBWaitForConnection, "Start emulation once the debugger is connected.");
#ifndef __ANDROID
OptionCheckbox("Serial Console", config::SerialConsole, "Dump the Dreamcast serial console to stdout");
OptionCheckbox("Serial PTY", config::SerialPTY, "Requires the option \"Serial Console\" to work");
#endif
static int gdbport = config::GDBPort;
if (ImGui::InputInt("GDB port", &gdbport))
{
config::GDBPort = gdbport;
}
const ImGuiStyle& style = ImGui::GetStyle();
ImGui::SameLine(0, style.ItemInnerSpacing.x);
ShowHelpMarker("Default port is 3263");
}
#endif
ImGui::Spacing();
#endif
header(T("Other"));
{
OptionCheckbox(T("HLE BIOS"), config::UseReios, T("Force high-level BIOS emulation"));
OptionCheckbox(T("Multi-threaded emulation"), config::ThreadedRendering,
T("Run the emulated CPU and GPU on different threads"));
#if !defined(__ANDROID) && !defined(GDB_SERVER)
OptionCheckbox(T("Serial Console"), config::SerialConsole,
T("Dump the Dreamcast serial console to stdout"));
#endif
{
DisabledScope scope(game_started);
OptionCheckbox(T("Dreamcast 32MB RAM Mod"), config::RamMod32MB,
T("Enables 32MB RAM Mod for Dreamcast. May affect compatibility"));
}
OptionCheckbox(T("Dump Textures"), config::DumpTextures,
T("Dump all textures into data/texdump/"));
ImGui::Indent();
{
DisabledScope scope(!config::DumpTextures.get());
OptionCheckbox(T("Dump Replaced Textures"), config::DumpReplacedTextures,
T("Always dump textures that are already replaced by custom textures"));
OptionCheckbox(T("Discard Video and Animated Textures"), config::DumpUniqueTextures,
T("Skip dumping video (YUV) and already updated textures"));
}
ImGui::Unindent();
bool logToFile = config::loadBool("log", "LogToFile", false);
if (ImGui::Checkbox(T("Log to File"), &logToFile))
config::saveBool("log", "LogToFile", logToFile);
ImGui::SameLine();
ShowHelpMarker(T("Log debug information to flycast.log"));
#ifdef SENTRY_UPLOAD
OptionCheckbox(T("Automatically Report Crashes"), config::UploadCrashLogs,
T("Automatically upload crash reports to sentry.io to help in troubleshooting. No personal information is included."));
#endif
}
#ifdef USE_LUA
header(T("Lua Scripting"));
{
InputText(T("Lua Filename"), &config::LuaFileName.get(), ImGuiInputTextFlags_CharsNoBlank);
ImGui::SameLine();
ShowHelpMarker(T("Specify lua filename to use. Should be located in Flycast config folder. Defaults to flycast.lua when empty."));
}
#endif
}
#if !defined(NDEBUG) || defined(DEBUGFAST) || FC_PROFILER
static void gui_debug_tab()
{
header("Logging");
{
LogManager *logManager = LogManager::GetInstance();
for (LogTypes::LOG_TYPE type = LogTypes::AICA; type < LogTypes::NUMBER_OF_LOGS; type = (LogTypes::LOG_TYPE)(type + 1))
{
bool enabled = logManager->IsEnabled(type, logManager->GetLogLevel());
std::string name = std::string(logManager->GetShortName(type)) + " - " + logManager->GetFullName(type);
if (ImGui::Checkbox(name.c_str(), &enabled) && logManager->GetLogLevel() > LogTypes::LWARNING) {
logManager->SetEnable(type, enabled);
config::saveBool("log", logManager->GetShortName(type), enabled);
}
}
ImGui::Spacing();
static const char *levels[] = { "Notice", "Error", "Warning", "Info", "Debug" };
if (ImGui::BeginCombo("Log Verbosity", levels[logManager->GetLogLevel() - 1], ImGuiComboFlags_None))
{
for (std::size_t i = 0; i < std::size(levels); i++)
{
bool is_selected = logManager->GetLogLevel() - 1 == (int)i;
if (ImGui::Selectable(levels[i], &is_selected)) {
logManager->SetLogLevel((LogTypes::LOG_LEVELS)(i + 1));
config::saveInt("log", "Verbosity", i + 1);
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
InputText("Log Server", &config::LogServer.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, dnsCharFilter);
ImGui::SameLine();
ShowHelpMarker("Log to this hostname[:port] with UDP. Default port is 31667.");
}
#if FC_PROFILER
ImGui::Spacing();
header("Profiling");
{
OptionCheckbox("Enable", config::ProfilerEnabled, "Enable the profiler.");
if (!config::ProfilerEnabled)
{
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
OptionCheckbox("Display", config::ProfilerDrawToGUI, "Draw the profiler output in an overlay.");
OptionCheckbox("Output to terminal", config::ProfilerOutputTTY, "Write the profiler output to the terminal");
// TODO frame warning time
if (!config::ProfilerEnabled)
{
ImGui::PopItemFlag();
ImGui::PopStyleVar();
}
}
#endif
}
#endif
static bool beginTabItem(const char *icon, const char *label) {
return ImGui::BeginTabItem((std::string(icon) + " " + label).c_str());
}
void gui_display_settings()
{
static std::array mapleDevicesChanges;
static std::array, 4> expDevicesChanges;
fullScreenWindow(false);
ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
ImGui::Begin(T("Settings"), nullptr, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse);
ImVec2 normal_padding = ImGui::GetStyle().FramePadding;
if (ImGui::Button(T("Done"), ScaledVec2(100, 30)))
{
if (uiUserScaleUpdated)
{
uiUserScaleUpdated = false;
mainui_reinit();
}
if (game_started)
gui_setState(GuiState::Commands);
else
gui_setState(GuiState::Main);
dreampotato::update();
if (game_started && settings.platform.isConsole())
{
for (unsigned bus = 0; bus < mapleDevicesChanges.size(); bus++)
{
if (mapleDevicesChanges[bus]) {
maple_ReconnectDevice(bus, 5);
}
else
{
if (expDevicesChanges[bus][0]) {
maple_ReconnectDevice(bus, 0);
reset_vmus();
}
if (expDevicesChanges[bus][1]) {
maple_ReconnectDevice(bus, 1);
reset_vmus();
}
}
}
}
mapleDevicesChanges = {};
expDevicesChanges = {};
SaveSettings();
}
if (game_started)
{
ImGui::SameLine();
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ImVec2(uiScaled(16), normal_padding.y));
if (config::Settings::instance().hasPerGameConfig())
{
if (ImGui::Button(T("Delete Game Config"), ScaledVec2(0, 30)))
{
config::Settings::instance().setPerGameConfig(false);
config::Settings::instance().load(false);
loadGameSpecificSettings();
}
}
else
{
if (ImGui::Button(T("Make Game Config"), ScaledVec2(0, 30)))
config::Settings::instance().setPerGameConfig(true);
}
}
if (ImGui::GetContentRegionAvail().x >= uiScaled(650.f))
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(16, 6));
else
// low width
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ScaledVec2(4, 6));
if (ImGui::BeginTabBar("settings", ImGuiTabBarFlags_NoTooltip | ImGuiTabBarFlags_NoTabListScrollingButtons))
{
if (beginTabItem(ICON_FA_TOOLBOX, T("General")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_general();
ImGui::EndTabItem();
}
if (beginTabItem(ICON_FA_GAMEPAD, T("Controls")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_controls(mapleDevicesChanges, expDevicesChanges);
ImGui::EndTabItem();
}
if (beginTabItem(ICON_FA_DISPLAY, T("Video")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_video();
ImGui::EndTabItem();
}
if (beginTabItem(ICON_FA_MUSIC, T("Audio")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_audio();
ImGui::EndTabItem();
}
if (beginTabItem(ICON_FA_WIFI, T("Network")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_network();
ImGui::EndTabItem();
}
if (beginTabItem(ICON_FA_MICROCHIP, T("Advanced")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_advanced();
ImGui::EndTabItem();
}
#if !defined(NDEBUG) || defined(DEBUGFAST) || FC_PROFILER
if (beginTabItem(ICON_FA_BUG, "Debug"))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_debug_tab();
ImGui::EndTabItem();
}
#endif
if (beginTabItem(ICON_FA_CIRCLE_INFO, T("About")))
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, normal_padding);
gui_settings_about();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::PopStyleVar();
scrollWhenDraggingOnVoid();
windowDragScroll();
ImGui::End();
}