mirror of
https://github.com/xemu-project/xemu.git
synced 2024-11-23 19:49:43 +00:00
2090 lines
72 KiB
C++
2090 lines
72 KiB
C++
/*
|
|
* xemu User Interface
|
|
*
|
|
* Copyright (C) 2020-2021 Matt Borgerson
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <SDL.h>
|
|
#include <epoxy/gl.h>
|
|
#include <stdio.h>
|
|
#include <deque>
|
|
|
|
#include "xemu-hud.h"
|
|
#include "xemu-input.h"
|
|
#include "xemu-notifications.h"
|
|
#include "xemu-settings.h"
|
|
#include "xemu-shaders.h"
|
|
#include "xemu-custom-widgets.h"
|
|
#include "xemu-monitor.h"
|
|
#include "xemu-version.h"
|
|
#include "xemu-data.h"
|
|
#include "xemu-net.h"
|
|
#include "xemu-os-utils.h"
|
|
#include "xemu-xbe.h"
|
|
#include "xemu-reporting.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include "imgui/backends/imgui_impl_sdl.h"
|
|
#include "imgui/backends/imgui_impl_opengl3.h"
|
|
#include "implot/implot.h"
|
|
|
|
extern "C" {
|
|
#include "noc_file_dialog.h"
|
|
|
|
// Include necessary QEMU headers
|
|
#include "qemu/osdep.h"
|
|
#include "qemu-common.h"
|
|
#include "sysemu/sysemu.h"
|
|
#include "sysemu/runstate.h"
|
|
#include "hw/xbox/mcpx/apu_debug.h"
|
|
#include "hw/xbox/nv2a/debug.h"
|
|
|
|
#undef typename
|
|
#undef atomic_fetch_add
|
|
#undef atomic_fetch_and
|
|
#undef atomic_fetch_xor
|
|
#undef atomic_fetch_or
|
|
#undef atomic_fetch_sub
|
|
}
|
|
|
|
ImFont *g_fixed_width_font;
|
|
float g_main_menu_height;
|
|
float g_ui_scale = 1.0;
|
|
bool g_trigger_style_update = true;
|
|
|
|
class NotificationManager
|
|
{
|
|
private:
|
|
const int kNotificationDuration = 4000;
|
|
std::deque<const char *> notification_queue;
|
|
bool active;
|
|
uint32_t notification_end_ts;
|
|
const char *msg;
|
|
|
|
public:
|
|
NotificationManager()
|
|
{
|
|
active = false;
|
|
}
|
|
|
|
~NotificationManager()
|
|
{
|
|
|
|
}
|
|
|
|
void QueueNotification(const char *msg)
|
|
{
|
|
notification_queue.push_back(strdup(msg));
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
uint32_t now = SDL_GetTicks();
|
|
|
|
if (active) {
|
|
// Currently displaying a notification
|
|
float t = (notification_end_ts - now)/(float)kNotificationDuration;
|
|
if (t > 1.0) {
|
|
// Notification delivered, free it
|
|
free((void*)msg);
|
|
active = false;
|
|
} else {
|
|
// Notification should be displayed
|
|
DrawNotification(t, msg);
|
|
}
|
|
} else {
|
|
// Check to see if a notification is pending
|
|
if (notification_queue.size() > 0) {
|
|
msg = notification_queue[0];
|
|
active = true;
|
|
notification_end_ts = now + kNotificationDuration;
|
|
notification_queue.pop_front();
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
void DrawNotification(float t, const char *msg)
|
|
{
|
|
const float DISTANCE = 10.0f;
|
|
static int corner = 1;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (corner != -1)
|
|
{
|
|
ImVec2 window_pos = ImVec2((corner & 1) ? io.DisplaySize.x - DISTANCE : DISTANCE, (corner & 2) ? io.DisplaySize.y - DISTANCE : DISTANCE);
|
|
window_pos.y = g_main_menu_height + DISTANCE;
|
|
ImVec2 window_pos_pivot = ImVec2((corner & 1) ? 1.0f : 0.0f, (corner & 2) ? 1.0f : 0.0f);
|
|
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot);
|
|
}
|
|
|
|
const float fade_in = 0.1;
|
|
const float fade_out = 0.9;
|
|
float fade = 0;
|
|
|
|
if (t < fade_in) {
|
|
// Linear fade in
|
|
fade = t/fade_in;
|
|
} else if (t >= fade_out) {
|
|
// Linear fade out
|
|
fade = 1-(t-fade_out)/(1-fade_out);
|
|
} else {
|
|
// Constant
|
|
fade = 1.0;
|
|
}
|
|
|
|
ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_ButtonActive];
|
|
color.w *= fade;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1);
|
|
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0,0,0,fade*0.9f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, color);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
ImGui::SetNextWindowBgAlpha(0.90f * fade);
|
|
if (ImGui::Begin("Notification", NULL,
|
|
ImGuiWindowFlags_Tooltip |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_NoFocusOnAppearing |
|
|
ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoInputs
|
|
))
|
|
{
|
|
ImGui::Text("%s", msg);
|
|
}
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
static void HelpMarker(const char* desc)
|
|
{
|
|
ImGui::TextDisabled("(?)");
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::BeginTooltip();
|
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
|
ImGui::TextUnformatted(desc);
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
class MonitorWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
private:
|
|
char InputBuf[256];
|
|
ImVector<char*> Items;
|
|
ImVector<const char*> Commands;
|
|
ImVector<char*> History;
|
|
int HistoryPos; // -1: new line, 0..History.Size-1 browsing history.
|
|
ImGuiTextFilter Filter;
|
|
bool AutoScroll;
|
|
bool ScrollToBottom;
|
|
|
|
public:
|
|
MonitorWindow()
|
|
{
|
|
is_open = false;
|
|
memset(InputBuf, 0, sizeof(InputBuf));
|
|
HistoryPos = -1;
|
|
AutoScroll = true;
|
|
ScrollToBottom = false;
|
|
}
|
|
~MonitorWindow()
|
|
{
|
|
}
|
|
|
|
// Portable helpers
|
|
static int Stricmp(const char* str1, const char* str2) { int d; while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } return d; }
|
|
static char* Strdup(const char *str) { size_t len = strlen(str) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)str, len); }
|
|
static void Strtrim(char* str) { char* str_end = str + strlen(str); while (str_end > str && str_end[-1] == ' ') str_end--; *str_end = 0; }
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(520*g_ui_scale, 600*g_ui_scale), ImGuiCond_FirstUseEver);
|
|
if (!ImGui::Begin("Monitor", &is_open))
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); // 1 separator, 1 input text
|
|
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); // Leave room for 1 separator + 1 InputText
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
ImGui::TextUnformatted(xemu_get_monitor_buffer());
|
|
ImGui::PopFont();
|
|
|
|
if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()))
|
|
ImGui::SetScrollHereY(1.0f);
|
|
ScrollToBottom = false;
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::EndChild();
|
|
ImGui::Separator();
|
|
|
|
// Command-line
|
|
bool reclaim_focus = false;
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
if (ImGui::InputText("", InputBuf, IM_ARRAYSIZE(InputBuf), ImGuiInputTextFlags_EnterReturnsTrue|ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_CallbackHistory, &TextEditCallbackStub, (void*)this))
|
|
{
|
|
char* s = InputBuf;
|
|
Strtrim(s);
|
|
if (s[0])
|
|
ExecCommand(s);
|
|
strcpy(s, "");
|
|
reclaim_focus = true;
|
|
}
|
|
ImGui::PopFont();
|
|
|
|
// Auto-focus on window apparition
|
|
ImGui::SetItemDefaultFocus();
|
|
if (reclaim_focus)
|
|
ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
private:
|
|
void ExecCommand(const char* command_line)
|
|
{
|
|
xemu_run_monitor_command(command_line);
|
|
|
|
// Insert into history. First find match and delete it so it can be pushed to the back. This isn't trying to be smart or optimal.
|
|
HistoryPos = -1;
|
|
for (int i = History.Size-1; i >= 0; i--)
|
|
if (Stricmp(History[i], command_line) == 0)
|
|
{
|
|
free(History[i]);
|
|
History.erase(History.begin() + i);
|
|
break;
|
|
}
|
|
History.push_back(Strdup(command_line));
|
|
|
|
// On commad input, we scroll to bottom even if AutoScroll==false
|
|
ScrollToBottom = true;
|
|
}
|
|
|
|
static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) // In C++11 you are better off using lambdas for this sort of forwarding callbacks
|
|
{
|
|
MonitorWindow* console = (MonitorWindow*)data->UserData;
|
|
return console->TextEditCallback(data);
|
|
}
|
|
|
|
int TextEditCallback(ImGuiInputTextCallbackData* data)
|
|
{
|
|
switch (data->EventFlag)
|
|
{
|
|
case ImGuiInputTextFlags_CallbackHistory:
|
|
{
|
|
// Example of HISTORY
|
|
const int prev_history_pos = HistoryPos;
|
|
if (data->EventKey == ImGuiKey_UpArrow)
|
|
{
|
|
if (HistoryPos == -1)
|
|
HistoryPos = History.Size - 1;
|
|
else if (HistoryPos > 0)
|
|
HistoryPos--;
|
|
}
|
|
else if (data->EventKey == ImGuiKey_DownArrow)
|
|
{
|
|
if (HistoryPos != -1)
|
|
if (++HistoryPos >= History.Size)
|
|
HistoryPos = -1;
|
|
}
|
|
|
|
// A better implementation would preserve the data on the current input line along with cursor position.
|
|
if (prev_history_pos != HistoryPos)
|
|
{
|
|
const char* history_str = (HistoryPos >= 0) ? History[HistoryPos] : "";
|
|
data->DeleteChars(0, data->BufTextLen);
|
|
data->InsertChars(0, history_str);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
class InputWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
InputWindow()
|
|
{
|
|
is_open = false;
|
|
}
|
|
|
|
~InputWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(500.0f*g_ui_scale, 0.0f));
|
|
// Remove window X padding for this window to easily center stuff
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,ImGui::GetStyle().WindowPadding.y));
|
|
if (!ImGui::Begin("Input", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
return;
|
|
}
|
|
|
|
static int active = 0;
|
|
|
|
// Output dimensions of texture
|
|
float t_w = 512, t_h = 512;
|
|
// Dimensions of (port+label)s
|
|
float b_x = 0, b_x_stride = 100, b_y = 400;
|
|
float b_w = 68, b_h = 81;
|
|
// Dimensions of controller (rendered at origin)
|
|
float controller_width = 477.0f;
|
|
float controller_height = 395.0f;
|
|
|
|
// Setup rendering to fbo for controller and port images
|
|
ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(controller_fbo);
|
|
|
|
//
|
|
// Render buttons with icons of the Xbox style port sockets with
|
|
// circular numbers above them. These buttons can be activated to
|
|
// configure the associated port, like a tabbed interface.
|
|
//
|
|
ImVec4 color_active(0.50, 0.86, 0.54, 0.12);
|
|
ImVec4 color_inactive(0, 0, 0, 0);
|
|
|
|
// Begin a 4-column layout to render the ports
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,12));
|
|
ImGui::Columns(4, "mixed", false);
|
|
|
|
const int port_padding = 8;
|
|
for (int i = 0; i < 4; i++) {
|
|
bool is_currently_selected = (i == active);
|
|
bool port_is_bound = (xemu_input_get_bound(i) != NULL);
|
|
|
|
// Set an X offset to center the image button within the column
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-b_w*g_ui_scale-2*port_padding*g_ui_scale)/2));
|
|
|
|
// We are using the same texture for all buttons, but ImageButton
|
|
// uses the texture as a unique ID. Push a new ID now to resolve
|
|
// the conflict.
|
|
ImGui::PushID(i);
|
|
float x = b_x+i*b_x_stride;
|
|
ImGui::PushStyleColor(ImGuiCol_Button, is_currently_selected ? color_active : color_inactive);
|
|
bool activated = ImGui::ImageButton(id,
|
|
ImVec2(b_w*g_ui_scale,b_h*g_ui_scale),
|
|
ImVec2(x/t_w, (b_y+b_h)/t_h),
|
|
ImVec2((x+b_w)/t_w, b_y/t_h),
|
|
port_padding);
|
|
ImGui::PopStyleColor();
|
|
|
|
if (activated) {
|
|
active = i;
|
|
}
|
|
|
|
uint32_t port_color = 0xafafafff;
|
|
bool is_hovered = ImGui::IsItemHovered();
|
|
if (is_currently_selected || port_is_bound) {
|
|
port_color = 0x81dc8a00;
|
|
} else if (is_hovered) {
|
|
port_color = 0x000000ff;
|
|
}
|
|
|
|
render_controller_port(x, b_y, i, port_color);
|
|
|
|
ImGui::PopID();
|
|
ImGui::NextColumn();
|
|
}
|
|
ImGui::PopStyleVar(); // ItemSpacing
|
|
ImGui::Columns(1);
|
|
|
|
//
|
|
// Render input device combo
|
|
//
|
|
|
|
// Center the combo above the controller with the same width
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width*g_ui_scale)/2.0));
|
|
|
|
// Note: SetNextItemWidth applies only to the combo element, but not the
|
|
// associated label which follows, so scale back a bit to make space for
|
|
// the label.
|
|
ImGui::SetNextItemWidth(controller_width*0.75*g_ui_scale);
|
|
|
|
// List available input devices
|
|
const char *not_connected = "Not Connected";
|
|
ControllerState *bound_state = xemu_input_get_bound(active);
|
|
|
|
// Get current controller name
|
|
const char *name;
|
|
if (bound_state == NULL) {
|
|
name = not_connected;
|
|
} else {
|
|
name = bound_state->name;
|
|
}
|
|
|
|
if (ImGui::BeginCombo("Input Devices", name))
|
|
{
|
|
// Handle "Not connected"
|
|
bool is_selected = bound_state == NULL;
|
|
if (ImGui::Selectable(not_connected, is_selected)) {
|
|
xemu_input_bind(active, NULL, 1);
|
|
bound_state = NULL;
|
|
}
|
|
if (is_selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
|
|
// Handle all available input devices
|
|
ControllerState *iter;
|
|
QTAILQ_FOREACH(iter, &available_controllers, entry) {
|
|
is_selected = bound_state == iter;
|
|
ImGui::PushID(iter);
|
|
const char *selectable_label = iter->name;
|
|
char buf[128];
|
|
if (iter->bound >= 0) {
|
|
snprintf(buf, sizeof(buf), "%s (Port %d)", iter->name, iter->bound+1);
|
|
selectable_label = buf;
|
|
}
|
|
if (ImGui::Selectable(selectable_label, is_selected)) {
|
|
xemu_input_bind(active, iter, 1);
|
|
bound_state = iter;
|
|
}
|
|
if (is_selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
ImGui::Columns(1);
|
|
|
|
//
|
|
// Add a separator between input selection and controller graphic
|
|
//
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
|
|
//
|
|
// Render controller image
|
|
//
|
|
bool device_selected = false;
|
|
|
|
if (bound_state) {
|
|
device_selected = true;
|
|
render_controller(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state);
|
|
} else {
|
|
static ControllerState state = { 0 };
|
|
render_controller(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state);
|
|
}
|
|
|
|
// update_sdl_controller_state(&state);
|
|
// update_sdl_kbd_controller_state(&state);
|
|
ImVec2 cur = ImGui::GetCursorPos();
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width*g_ui_scale)/2.0));
|
|
ImGui::Image(id,
|
|
ImVec2(controller_width*g_ui_scale, controller_height*g_ui_scale),
|
|
ImVec2(0, controller_height/t_h),
|
|
ImVec2(controller_width/t_w, 0));
|
|
|
|
if (!device_selected) {
|
|
// ImGui::SameLine();
|
|
const char *msg = "Please select an available input device";
|
|
ImVec2 dim = ImGui::CalcTextSize(msg);
|
|
ImGui::SetCursorPosX(cur.x + (controller_width*g_ui_scale-dim.x)/2);
|
|
ImGui::SetCursorPosY(cur.y + (controller_height*g_ui_scale-dim.y)/2);
|
|
ImGui::Text("%s", msg);
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(); // Window padding
|
|
|
|
// Restore original framebuffer target
|
|
render_to_default_fb();
|
|
}
|
|
};
|
|
|
|
static const char *paused_file_open(int flags,
|
|
const char *filters,
|
|
const char *default_path,
|
|
const char *default_name)
|
|
{
|
|
bool is_running = runstate_is_running();
|
|
if (is_running) {
|
|
vm_stop(RUN_STATE_PAUSED);
|
|
}
|
|
const char *r = noc_file_dialog_open(flags, filters, default_path, default_name);
|
|
if (is_running) {
|
|
vm_start();
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
#define MAX_STRING_LEN 2048 // FIXME: Completely arbitrary and only used here
|
|
// to give a buffer to ImGui for each field
|
|
|
|
class SettingsWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
private:
|
|
bool dirty;
|
|
bool pending_restart;
|
|
|
|
char flash_path[MAX_STRING_LEN];
|
|
char bootrom_path[MAX_STRING_LEN];
|
|
char hdd_path[MAX_STRING_LEN];
|
|
char eeprom_path[MAX_STRING_LEN];
|
|
int memory_idx;
|
|
bool short_animation;
|
|
|
|
public:
|
|
SettingsWindow()
|
|
{
|
|
is_open = false;
|
|
dirty = false;
|
|
pending_restart = false;
|
|
|
|
flash_path[0] = '\0';
|
|
bootrom_path[0] = '\0';
|
|
hdd_path[0] = '\0';
|
|
eeprom_path[0] = '\0';
|
|
memory_idx = 0;
|
|
short_animation = false;
|
|
}
|
|
|
|
~SettingsWindow()
|
|
{
|
|
}
|
|
|
|
void Load()
|
|
{
|
|
const char *tmp;
|
|
int tmp_int;
|
|
size_t len;
|
|
|
|
xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_FLASH_PATH, &tmp);
|
|
len = strlen(tmp);
|
|
assert(len < MAX_STRING_LEN);
|
|
strncpy(flash_path, tmp, sizeof(flash_path));
|
|
|
|
xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_BOOTROM_PATH, &tmp);
|
|
len = strlen(tmp);
|
|
assert(len < MAX_STRING_LEN);
|
|
strncpy(bootrom_path, tmp, sizeof(bootrom_path));
|
|
|
|
xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_HDD_PATH, &tmp);
|
|
len = strlen(tmp);
|
|
assert(len < MAX_STRING_LEN);
|
|
strncpy(hdd_path, tmp, sizeof(hdd_path));
|
|
|
|
xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_EEPROM_PATH, &tmp);
|
|
len = strlen(tmp);
|
|
assert(len < MAX_STRING_LEN);
|
|
strncpy(eeprom_path, tmp, sizeof(eeprom_path));
|
|
|
|
xemu_settings_get_int(XEMU_SETTINGS_SYSTEM_MEMORY, &tmp_int);
|
|
memory_idx = (tmp_int-64)/64;
|
|
|
|
xemu_settings_get_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, &tmp_int);
|
|
short_animation = !!tmp_int;
|
|
|
|
dirty = false;
|
|
}
|
|
|
|
void Save()
|
|
{
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_FLASH_PATH, flash_path);
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_BOOTROM_PATH, bootrom_path);
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_HDD_PATH, hdd_path);
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_EEPROM_PATH, eeprom_path);
|
|
xemu_settings_set_int(XEMU_SETTINGS_SYSTEM_MEMORY, 64+memory_idx*64);
|
|
xemu_settings_set_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, short_animation);
|
|
xemu_settings_save();
|
|
xemu_queue_notification("Settings saved! Restart to apply updates.");
|
|
pending_restart = true;
|
|
}
|
|
|
|
void FilePicker(const char *name, char *buf, size_t len, const char *filters)
|
|
{
|
|
ImGui::PushID(name);
|
|
if (ImGui::InputText("", buf, len)) {
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Browse...", ImVec2(100*g_ui_scale, 0))) {
|
|
const char *selected = paused_file_open(NOC_FILE_DIALOG_OPEN, filters, buf, NULL);
|
|
if ((selected != NULL) && (strcmp(buf, selected) != 0)) {
|
|
strncpy(buf, selected, len-1);
|
|
dirty = true;
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_ui_scale, 0.0f));
|
|
if (!ImGui::Begin("Settings", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsWindowAppearing()) {
|
|
Load();
|
|
}
|
|
|
|
const char *rom_file_filters = ".bin Files\0*.bin\0.rom Files\0*.rom\0All Files\0*.*\0";
|
|
const char *qcow_file_filters = ".qcow2 Files\0*.qcow2\0All Files\0*.*\0";
|
|
|
|
ImGui::Columns(2, "", false);
|
|
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.25);
|
|
|
|
ImGui::Text("Flash (BIOS) File");
|
|
ImGui::NextColumn();
|
|
float picker_width = ImGui::GetColumnWidth()-120*g_ui_scale;
|
|
ImGui::SetNextItemWidth(picker_width);
|
|
FilePicker("###Flash", flash_path, sizeof(flash_path), rom_file_filters);
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("MCPX Boot ROM File");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(picker_width);
|
|
FilePicker("###BootROM", bootrom_path, sizeof(bootrom_path), rom_file_filters);
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("Hard Disk Image File");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(picker_width);
|
|
FilePicker("###HDD", hdd_path, sizeof(hdd_path), qcow_file_filters);
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("EEPROM File");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(picker_width);
|
|
FilePicker("###EEPROM", eeprom_path, sizeof(eeprom_path), rom_file_filters);
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("System Memory");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(ImGui::GetColumnWidth()*0.5);
|
|
if (ImGui::Combo("###mem", &memory_idx, "64 MiB\0" "128 MiB\0")) {
|
|
dirty = true;
|
|
}
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Dummy(ImVec2(0,0));
|
|
ImGui::NextColumn();
|
|
if (ImGui::Checkbox("Skip startup animation", &short_animation)) {
|
|
dirty = true;
|
|
}
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Columns(1);
|
|
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
|
|
if (dirty) {
|
|
ImGui::Text("Warning: Unsaved changes!");
|
|
ImGui::SameLine();
|
|
} else if (pending_restart) {
|
|
ImGui::Text("Restart to apply updates");
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_ui_scale);
|
|
ImGui::SetItemDefaultFocus();
|
|
if (ImGui::Button("Save", ImVec2(120*g_ui_scale, 0))) {
|
|
Save();
|
|
dirty = false;
|
|
pending_restart = true;
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
class AboutWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
private:
|
|
char build_info_text[256];
|
|
|
|
public:
|
|
AboutWindow()
|
|
{
|
|
snprintf(build_info_text, sizeof(build_info_text),
|
|
"Version: %s\n" "Branch: %s\n" "Commit: %s\n" "Date: %s",
|
|
xemu_version, xemu_branch, xemu_commit, xemu_date);
|
|
// FIXME: Show platform
|
|
// FIXME: Show driver
|
|
// FIXME: Show BIOS/BootROM hash
|
|
}
|
|
|
|
~AboutWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(400.0f*g_ui_scale, 0.0f));
|
|
if (!ImGui::Begin("About", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
static uint32_t time_start = 0;
|
|
if (ImGui::IsWindowAppearing()) {
|
|
time_start = SDL_GetTicks();
|
|
}
|
|
uint32_t now = SDL_GetTicks() - time_start;
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50*g_ui_scale);
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_ui_scale)/2);
|
|
|
|
ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(logo_fbo);
|
|
float t_w = 256.0;
|
|
float t_h = 256.0;
|
|
float x_off = 0;
|
|
ImGui::Image(id,
|
|
ImVec2((t_w-x_off)*g_ui_scale, t_h*g_ui_scale),
|
|
ImVec2(x_off/t_w, t_h/t_h),
|
|
ImVec2(t_w/t_w, 0));
|
|
if (ImGui::IsItemClicked()) {
|
|
time_start = SDL_GetTicks();
|
|
}
|
|
render_logo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
|
|
render_to_default_fb();
|
|
ImGui::SetCursorPosX(10*g_ui_scale);
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100*g_ui_scale);
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(xemu_version).x)/2);
|
|
ImGui::Text("%s", xemu_version);
|
|
|
|
ImGui::SetCursorPosX(10*g_ui_scale);
|
|
ImGui::Dummy(ImVec2(0,20*g_ui_scale));
|
|
|
|
const char *msg = "Visit https://xemu.app for more information";
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
|
|
ImGui::Text("%s", msg);
|
|
if (ImGui::IsItemClicked()) {
|
|
xemu_open_web_browser("https://xemu.app");
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0,40*g_ui_scale));
|
|
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
ImGui::InputTextMultiline("##build_info", build_info_text, sizeof(build_info_text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 6), ImGuiInputTextFlags_ReadOnly);
|
|
ImGui::PopFont();
|
|
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
class NetworkWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
int backend;
|
|
char remote_addr[64];
|
|
char local_addr[64];
|
|
|
|
NetworkWindow()
|
|
{
|
|
is_open = false;
|
|
}
|
|
|
|
~NetworkWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(400.0f*g_ui_scale, 0.0f));
|
|
if (!ImGui::Begin("Network", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsWindowAppearing()) {
|
|
const char *tmp;
|
|
xemu_settings_get_string(XEMU_SETTINGS_NETWORK_REMOTE_ADDR, &tmp);
|
|
strncpy(remote_addr, tmp, sizeof(remote_addr)-1);
|
|
xemu_settings_get_string(XEMU_SETTINGS_NETWORK_LOCAL_ADDR, &tmp);
|
|
strncpy(local_addr, tmp, sizeof(local_addr)-1);
|
|
xemu_settings_get_enum(XEMU_SETTINGS_NETWORK_BACKEND, &backend);
|
|
}
|
|
|
|
ImGuiInputTextFlags flg = 0;
|
|
bool is_enabled = xemu_net_is_enabled();
|
|
if (is_enabled) {
|
|
flg |= ImGuiInputTextFlags_ReadOnly;
|
|
}
|
|
|
|
ImGui::Columns(2, "", false);
|
|
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.33);
|
|
|
|
ImGui::Text("Attached To");
|
|
ImGui::SameLine(); HelpMarker("The network backend which the emulated NIC interacts with");
|
|
ImGui::NextColumn();
|
|
if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
|
|
int temp_backend = backend; // Temporary to make backend combo read-only (FIXME: surely there's a nicer way)
|
|
if (ImGui::Combo("##backend", is_enabled ? &temp_backend : &backend, "User (NAT)\0Socket\0") && !is_enabled) {
|
|
xemu_settings_set_enum(XEMU_SETTINGS_NETWORK_BACKEND, backend);
|
|
xemu_settings_save();
|
|
}
|
|
if (is_enabled) ImGui::PopStyleVar();
|
|
ImGui::SameLine();
|
|
if (backend == XEMU_NET_BACKEND_USER) {
|
|
HelpMarker("User-mode TCP/IP stack with a NAT'd network");
|
|
} else if (backend == XEMU_NET_BACKEND_SOCKET_UDP) {
|
|
HelpMarker("Encapsulates link-layer traffic in UDP packets");
|
|
}
|
|
ImGui::NextColumn();
|
|
|
|
if (backend == XEMU_NET_BACKEND_SOCKET_UDP) {
|
|
ImGui::Text("Remote Host");
|
|
ImGui::SameLine(); HelpMarker("The remote <IP address>:<Port> to forward packets to (e.g. 1.2.3.4:9368)");
|
|
ImGui::NextColumn();
|
|
float w = ImGui::GetColumnWidth()-10*g_ui_scale;
|
|
ImGui::SetNextItemWidth(w);
|
|
if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
|
|
ImGui::InputText("###remote_host", remote_addr, sizeof(remote_addr), flg);
|
|
if (is_enabled) ImGui::PopStyleVar();
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("Local Host");
|
|
ImGui::SameLine(); HelpMarker("The local <IP address>:<Port> to receive packets on (e.g. 0.0.0.0:9368)");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(w);
|
|
if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
|
|
ImGui::InputText("###local_host", local_addr, sizeof(local_addr), flg);
|
|
if (is_enabled) ImGui::PopStyleVar();
|
|
ImGui::NextColumn();
|
|
}
|
|
|
|
ImGui::Columns(1);
|
|
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
|
|
ImGui::Text("Status: %sEnabled", is_enabled ? "" : "Not ");
|
|
ImGui::SameLine();
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_ui_scale);
|
|
ImGui::SetItemDefaultFocus();
|
|
if (ImGui::Button(is_enabled ? "Disable" : "Enable", ImVec2(120*g_ui_scale, 0))) {
|
|
if (!is_enabled) {
|
|
xemu_settings_set_string(XEMU_SETTINGS_NETWORK_REMOTE_ADDR, remote_addr);
|
|
xemu_settings_set_string(XEMU_SETTINGS_NETWORK_LOCAL_ADDR, local_addr);
|
|
xemu_net_enable();
|
|
} else {
|
|
xemu_net_disable();
|
|
}
|
|
xemu_settings_set_bool(XEMU_SETTINGS_NETWORK_ENABLED, xemu_net_is_enabled());
|
|
xemu_settings_save();
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
#ifdef CONFIG_CPUID_H
|
|
#include <cpuid.h>
|
|
#endif
|
|
|
|
const char *get_cpu_info(void)
|
|
{
|
|
const char *cpu_info = "";
|
|
#ifdef CONFIG_CPUID_H
|
|
static uint32_t brand[12];
|
|
if (__get_cpuid_max(0x80000004, NULL)) {
|
|
__get_cpuid(0x80000002, brand+0x0, brand+0x1, brand+0x2, brand+0x3);
|
|
__get_cpuid(0x80000003, brand+0x4, brand+0x5, brand+0x6, brand+0x7);
|
|
__get_cpuid(0x80000004, brand+0x8, brand+0x9, brand+0xa, brand+0xb);
|
|
}
|
|
cpu_info = (const char *)brand;
|
|
#endif
|
|
// FIXME: Support other architectures (e.g. ARM)
|
|
return cpu_info;
|
|
}
|
|
|
|
class CompatibilityReporter
|
|
{
|
|
public:
|
|
CompatibilityReport report;
|
|
bool dirty;
|
|
bool is_open;
|
|
bool is_xbe_identified;
|
|
bool did_send, send_result;
|
|
char token_buf[512];
|
|
int playability;
|
|
char description[1024];
|
|
std::string serialized_report;
|
|
|
|
CompatibilityReporter()
|
|
{
|
|
is_open = false;
|
|
|
|
report.token = "";
|
|
report.xemu_version = xemu_version;
|
|
report.xemu_branch = xemu_branch;
|
|
report.xemu_commit = xemu_commit;
|
|
report.xemu_date = xemu_date;
|
|
#if defined(__linux__)
|
|
report.os_platform = "Linux";
|
|
#elif defined(_WIN32)
|
|
report.os_platform = "Windows";
|
|
#elif defined(__APPLE__)
|
|
report.os_platform = "macOS";
|
|
#else
|
|
report.os_platform = "Unknown";
|
|
#endif
|
|
report.os_version = xemu_get_os_info();
|
|
report.cpu = get_cpu_info();
|
|
dirty = true;
|
|
is_xbe_identified = false;
|
|
did_send = send_result = false;
|
|
}
|
|
|
|
~CompatibilityReporter()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
const char *playability_names[] = {
|
|
"Broken",
|
|
"Intro",
|
|
"Starts",
|
|
"Playable",
|
|
"Perfect",
|
|
};
|
|
|
|
const char *playability_descriptions[] = {
|
|
"This title crashes very soon after launching, or displays nothing at all.",
|
|
"This title displays an intro sequence, but fails to make it to gameplay.",
|
|
"This title starts, but may crash or have significant issues.",
|
|
"This title is playable, but may have minor issues.",
|
|
"This title is playable from start to finish with no noticable issues."
|
|
};
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_ui_scale, 0.0f));
|
|
if (!ImGui::Begin("Report Compatibility", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsWindowAppearing()) {
|
|
report.gl_vendor = (const char *)glGetString(GL_VENDOR);
|
|
report.gl_renderer = (const char *)glGetString(GL_RENDERER);
|
|
report.gl_version = (const char *)glGetString(GL_VERSION);
|
|
report.gl_shading_language_version = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
|
|
struct xbe *xbe = xemu_get_xbe_info();
|
|
is_xbe_identified = xbe != NULL;
|
|
if (is_xbe_identified) {
|
|
report.SetXbeData(xbe);
|
|
}
|
|
did_send = send_result = false;
|
|
|
|
playability = 3; // Playable
|
|
report.compat_rating = playability_names[playability];
|
|
description[0] = '\x00';
|
|
report.compat_comments = description;
|
|
|
|
const char *tmp;
|
|
xemu_settings_get_string(XEMU_SETTINGS_MISC_USER_TOKEN, &tmp);
|
|
assert(strlen(tmp) < sizeof(token_buf));
|
|
strncpy(token_buf, tmp, sizeof(token_buf));
|
|
report.token = token_buf;
|
|
|
|
dirty = true;
|
|
}
|
|
|
|
if (!is_xbe_identified) {
|
|
ImGui::TextWrapped(
|
|
"An XBE could not be identified. Please launch an official "
|
|
"Xbox title to submit a compatibility report.");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
ImGui::TextWrapped(
|
|
"If you would like to help improve xemu by submitting a compatibility report for this "
|
|
"title, please select an appropriate playability level, enter a "
|
|
"brief description, then click 'Send'."
|
|
"\n\n"
|
|
"Note: By submitting a report, you acknowledge and consent to "
|
|
"collection, archival, and publication of information as outlined "
|
|
"in 'Privacy Disclosure' below.");
|
|
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
|
|
ImGui::Columns(2, "", false);
|
|
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.25);
|
|
|
|
ImGui::Text("User Token");
|
|
ImGui::SameLine();
|
|
HelpMarker("This is a unique access token used to authorize submission of the report. To request a token, click 'Get Token'.");
|
|
ImGui::NextColumn();
|
|
float item_width = ImGui::GetColumnWidth()*0.75-20*g_ui_scale;
|
|
ImGui::SetNextItemWidth(item_width);
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
if (ImGui::InputText("###UserToken", token_buf, sizeof(token_buf), 0)) {
|
|
report.token = token_buf;
|
|
dirty = true;
|
|
}
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Get Token")) {
|
|
xemu_open_web_browser("https://reports.xemu.app");
|
|
}
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Text("Playability");
|
|
ImGui::NextColumn();
|
|
ImGui::SetNextItemWidth(item_width);
|
|
if (ImGui::Combo("###PlayabilityRating", &playability,
|
|
"Broken\0" "Intro/Menus\0" "Starts\0" "Playable\0" "Perfect\0")) {
|
|
report.compat_rating = playability_names[playability];
|
|
dirty = true;
|
|
}
|
|
ImGui::SameLine();
|
|
HelpMarker(playability_descriptions[playability]);
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::Columns(1);
|
|
|
|
ImGui::Text("Description");
|
|
if (ImGui::InputTextMultiline("###desc", description, sizeof(description), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 6), 0)) {
|
|
report.compat_comments = description;
|
|
dirty = true;
|
|
}
|
|
|
|
if (ImGui::TreeNode("Report Details")) {
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
if (dirty) {
|
|
serialized_report = report.GetSerializedReport();
|
|
dirty = false;
|
|
}
|
|
ImGui::InputTextMultiline("##build_info", (char*)serialized_report.c_str(), strlen(serialized_report.c_str())+1, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 7), ImGuiInputTextFlags_ReadOnly);
|
|
ImGui::PopFont();
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
if (ImGui::TreeNode("Privacy Disclosure (Please read before submission!)")) {
|
|
ImGui::TextWrapped(
|
|
"By volunteering to submit a compatibility report, basic information about your "
|
|
"computer is collected, including: your operating system version, CPU model, "
|
|
"graphics card/driver information, and details about the title which are "
|
|
"extracted from the executable in memory. The contents of this report can be "
|
|
"seen before submission by expanding 'Report Details'."
|
|
"\n\n"
|
|
"Like many websites, upon submission, the public IP address of your computer is "
|
|
"also recorded with your report. If provided, the identity associated with your "
|
|
"token is also recorded."
|
|
"\n\n"
|
|
"This information will be archived and used to analyze, resolve problems with, "
|
|
"and improve the application. This information may be made publicly visible, "
|
|
"for example: to anyone who wishes to see the playability status of a title, as "
|
|
"indicated by your report.");
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
|
|
|
|
if (did_send) {
|
|
if (send_result) {
|
|
ImGui::Text("Sent! Thanks.");
|
|
} else {
|
|
ImGui::Text("Error: %s (%d)", report.GetResultMessage().c_str(), report.GetResultCode());
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_ui_scale);
|
|
|
|
ImGui::SetItemDefaultFocus();
|
|
if (ImGui::Button("Send", ImVec2(120*g_ui_scale, 0))) {
|
|
did_send = true;
|
|
send_result = report.Send();
|
|
if (send_result) {
|
|
// Close window on success
|
|
is_open = false;
|
|
|
|
// Save user token if it was used
|
|
xemu_settings_set_string(XEMU_SETTINGS_MISC_USER_TOKEN, token_buf);
|
|
xemu_settings_save();
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
#include <math.h>
|
|
|
|
float mix(float a, float b, float t)
|
|
{
|
|
return a*(1.0-t) + (b-a)*t;
|
|
}
|
|
|
|
class DebugApuWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
DebugApuWindow()
|
|
{
|
|
is_open = false;
|
|
}
|
|
|
|
~DebugApuWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(600.0f*g_ui_scale, 0.0f));
|
|
if (!ImGui::Begin("Audio Debug", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
const struct McpxApuDebug *dbg = mcpx_apu_get_debug_info();
|
|
|
|
|
|
ImGui::Columns(2, "", false);
|
|
int now = SDL_GetTicks() % 1000;
|
|
float t = now/1000.0f;
|
|
float freq = 1;
|
|
float v = fabs(sin(M_PI*t*freq));
|
|
float c_active = mix(0.4, 0.97, v);
|
|
float c_inactive = 0.2f;
|
|
|
|
int voice_monitor = -1;
|
|
int voice_info = -1;
|
|
int voice_mute = -1;
|
|
|
|
// Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style.
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
|
|
for (int i = 0; i < 256; i++)
|
|
{
|
|
if (i % 16) {
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
float c, s, h;
|
|
h = 0.6;
|
|
if (dbg->vp.v[i].active) {
|
|
if (dbg->vp.v[i].paused) {
|
|
c = c_inactive;
|
|
s = 0.4;
|
|
} else {
|
|
c = c_active;
|
|
s = 0.7;
|
|
}
|
|
if (mcpx_apu_debug_is_muted(i)) {
|
|
h = 1.0;
|
|
}
|
|
} else {
|
|
c = c_inactive;
|
|
s = 0;
|
|
}
|
|
|
|
ImGui::PushID(i);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(h, s, c));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(h, s, 0.8));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(h, 0.8f, 1.0));
|
|
char buf[12];
|
|
snprintf(buf, sizeof(buf), "%02x", i);
|
|
ImGui::Button(buf);
|
|
if (/*dbg->vp.v[i].active &&*/ ImGui::IsItemHovered()) {
|
|
voice_monitor = i;
|
|
voice_info = i;
|
|
}
|
|
if (ImGui::IsItemClicked(1)) {
|
|
voice_mute = i;
|
|
}
|
|
ImGui::PopStyleColor(3);
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::PopFont();
|
|
|
|
if (voice_info >= 0) {
|
|
const struct McpxApuDebugVoice *voice = &dbg->vp.v[voice_info];
|
|
ImGui::BeginTooltip();
|
|
bool is_paused = voice->paused;
|
|
ImGui::Text("Voice 0x%x/%d %s", voice_info, voice_info, is_paused ? "(Paused)" : "");
|
|
ImGui::SameLine();
|
|
ImGui::Text(voice->stereo ? "Stereo" : "Mono");
|
|
|
|
ImGui::Separator();
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
|
|
const char *noyes[2] = { "NO", "YES" };
|
|
ImGui::Text("Stream: %-3s Loop: %-3s Persist: %-3s Multipass: %-3s "
|
|
"Linked: %-3s",
|
|
noyes[voice->stream], noyes[voice->loop],
|
|
noyes[voice->persist], noyes[voice->multipass],
|
|
noyes[voice->linked]);
|
|
|
|
const char *cs[4] = { "1 byte", "2 bytes", "ADPCM", "4 bytes" };
|
|
const char *ss[4] = {
|
|
"Unsigned 8b PCM",
|
|
"Signed 16b PCM",
|
|
"Signed 24b PCM",
|
|
"Signed 32b PCM"
|
|
};
|
|
|
|
assert(voice->container_size < 4);
|
|
assert(voice->sample_size < 4);
|
|
ImGui::Text("Container Size: %s, Sample Size: %s, Samples per Block: %d",
|
|
cs[voice->container_size], ss[voice->sample_size], voice->samples_per_block);
|
|
ImGui::Text("Rate: %f (%d Hz)", voice->rate, (int)(48000.0/voice->rate));
|
|
ImGui::Text("EBO=%d CBO=%d LBO=%d BA=%x",
|
|
voice->ebo, voice->cbo, voice->lbo, voice->ba);
|
|
ImGui::Text("Mix: ");
|
|
for (int i = 0; i < 8; i++) {
|
|
if (i == 4) ImGui::Text(" ");
|
|
ImGui::SameLine();
|
|
char buf[64];
|
|
if (voice->vol[i] == 0xFFF) {
|
|
snprintf(buf, sizeof(buf),
|
|
"Bin %2d (MUTE) ", voice->bin[i]);
|
|
} else {
|
|
snprintf(buf, sizeof(buf),
|
|
"Bin %2d (-%.3f) ", voice->bin[i],
|
|
(float)((voice->vol[i] >> 6) & 0x3f) +
|
|
(float)((voice->vol[i] >> 0) & 0x3f) / 64.0);
|
|
}
|
|
ImGui::Text("%-17s", buf);
|
|
}
|
|
ImGui::PopFont();
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
if (voice_monitor >= 0) {
|
|
mcpx_apu_debug_isolate_voice(voice_monitor);
|
|
} else {
|
|
mcpx_apu_debug_clear_isolations();
|
|
}
|
|
if (voice_mute >= 0) {
|
|
mcpx_apu_debug_toggle_mute(voice_mute);
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
ImGui::SetColumnWidth(0, ImGui::GetCursorPosX());
|
|
ImGui::NextColumn();
|
|
|
|
ImGui::PushFont(g_fixed_width_font);
|
|
ImGui::Text("Frames: %04d", dbg->frames_processed);
|
|
ImGui::Text("GP Cycles: %04d", dbg->gp.cycles);
|
|
ImGui::Text("EP Cycles: %04d", dbg->ep.cycles);
|
|
bool color = (dbg->utilization > 0.9);
|
|
if (color) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
|
|
ImGui::Text("Utilization: %.2f%%", (dbg->utilization*100));
|
|
if (color) ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Separator();
|
|
|
|
static int mon = 0;
|
|
mon = mcpx_apu_debug_get_monitor();
|
|
if (ImGui::Combo("Monitor", &mon, "AC97\0VP Only\0GP Only\0EP Only\0GP/EP if enabled\0")) {
|
|
mcpx_apu_debug_set_monitor(mon);
|
|
}
|
|
|
|
static bool gp_realtime;
|
|
gp_realtime = dbg->gp_realtime;
|
|
if (ImGui::Checkbox("GP Realtime\n", &gp_realtime)) {
|
|
mcpx_apu_debug_set_gp_realtime_enabled(gp_realtime);
|
|
}
|
|
|
|
static bool ep_realtime;
|
|
ep_realtime = dbg->ep_realtime;
|
|
if (ImGui::Checkbox("EP Realtime\n", &ep_realtime)) {
|
|
mcpx_apu_debug_set_ep_realtime_enabled(ep_realtime);
|
|
}
|
|
|
|
ImGui::Columns(1);
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// utility structure for realtime plot
|
|
struct ScrollingBuffer {
|
|
int MaxSize;
|
|
int Offset;
|
|
ImVector<ImVec2> Data;
|
|
ScrollingBuffer() {
|
|
MaxSize = 2000;
|
|
Offset = 0;
|
|
Data.reserve(MaxSize);
|
|
}
|
|
void AddPoint(float x, float y) {
|
|
if (Data.size() < MaxSize)
|
|
Data.push_back(ImVec2(x,y));
|
|
else {
|
|
Data[Offset] = ImVec2(x,y);
|
|
Offset = (Offset + 1) % MaxSize;
|
|
}
|
|
}
|
|
void Erase() {
|
|
if (Data.size() > 0) {
|
|
Data.shrink(0);
|
|
Offset = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
class DebugVideoWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
bool transparent;
|
|
|
|
DebugVideoWindow()
|
|
{
|
|
is_open = false;
|
|
transparent = false;
|
|
}
|
|
|
|
~DebugVideoWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
float alpha = transparent ? 0.2 : 1.0;
|
|
|
|
ImVec4 c;
|
|
|
|
c = ImGui::GetStyle().Colors[transparent ? ImGuiCol_WindowBg : ImGuiCol_TitleBg];
|
|
c.w *= alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBg, c);
|
|
|
|
c = ImGui::GetStyle().Colors[transparent ? ImGuiCol_WindowBg : ImGuiCol_TitleBgActive];
|
|
c.w *= alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, c);
|
|
|
|
c = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
|
|
c.w *= alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, c);
|
|
|
|
c = ImGui::GetStyle().Colors[ImGuiCol_Border];
|
|
c.w *= alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_Border, c);
|
|
|
|
c = ImGui::GetStyle().Colors[ImGuiCol_FrameBg];
|
|
c.w *= alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, c);
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(600.0f*g_ui_scale, 150.0f*g_ui_scale), ImGuiCond_Once);
|
|
if (ImGui::Begin("Video Debug", &is_open)) {
|
|
|
|
double x_start, x_end;
|
|
static ImPlotAxisFlags rt_axis = ImPlotAxisFlags_NoTickLabels;
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(5,5));
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f);
|
|
static ScrollingBuffer fps;
|
|
static float t = 0;
|
|
if (runstate_is_running()) {
|
|
t += ImGui::GetIO().DeltaTime;
|
|
fps.AddPoint(t, g_nv2a_stats.increment_fps);
|
|
}
|
|
x_start = t - 10.0;
|
|
x_end = t;
|
|
ImPlot::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
|
|
ImPlot::SetNextPlotLimitsY(0, 65, ImGuiCond_Always);
|
|
|
|
float plot_width = 0.5 * (ImGui::GetWindowSize().x -
|
|
2 * ImGui::GetStyle().WindowPadding.x -
|
|
ImGui::GetStyle().ItemSpacing.x);
|
|
|
|
ImGui::SetNextWindowBgAlpha(alpha);
|
|
if (ImPlot::BeginPlot("##ScrollingFPS", NULL, NULL, ImVec2(plot_width,75*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
|
|
if (fps.Data.size() > 0) {
|
|
ImPlot::PlotShaded("##fps", &fps.Data[0].x, &fps.Data[0].y, fps.Data.size(), 0, fps.Offset, 2 * sizeof(float));
|
|
ImPlot::PlotLine("##fps", &fps.Data[0].x, &fps.Data[0].y, fps.Data.size(), fps.Offset, 2 * sizeof(float));
|
|
}
|
|
ImPlot::AnnotateClamped(x_start, 65, ImVec2(0,0), ImPlot::GetLastItemColor(), "FPS: %d", g_nv2a_stats.increment_fps);
|
|
ImPlot::EndPlot();
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
x_end = g_nv2a_stats.frame_count;
|
|
x_start = x_end - NV2A_PROF_NUM_FRAMES;
|
|
|
|
ImPlot::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
|
|
ImPlot::SetNextPlotLimitsY(0, 100, ImGuiCond_Always);
|
|
ImPlot::PushStyleColor(ImPlotCol_Line, ImPlot::GetColormapColor(1));
|
|
ImGui::SetNextWindowBgAlpha(alpha);
|
|
if (ImPlot::BeginPlot("##ScrollingMSPF", NULL, NULL, ImVec2(plot_width,75*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
|
|
ImPlot::PlotShaded("##mspf", &g_nv2a_stats.frame_history[0].mspf, NV2A_PROF_NUM_FRAMES, 0, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
|
|
ImPlot::PlotLine("##mspf", &g_nv2a_stats.frame_history[0].mspf, NV2A_PROF_NUM_FRAMES, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
|
|
ImPlot::AnnotateClamped(x_start, 100, ImVec2(0,0), ImPlot::GetLastItemColor(), "MSPF: %d", g_nv2a_stats.frame_history[(g_nv2a_stats.frame_ptr - 1) % NV2A_PROF_NUM_FRAMES].mspf);
|
|
ImPlot::EndPlot();
|
|
}
|
|
ImPlot::PopStyleColor();
|
|
|
|
if (ImGui::TreeNode("Advanced")) {
|
|
ImPlot::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
|
|
ImPlot::SetNextPlotLimitsY(0, 1500, ImGuiCond_Always);
|
|
ImGui::SetNextWindowBgAlpha(alpha);
|
|
if (ImPlot::BeginPlot("##ScrollingDraws", NULL, NULL, ImVec2(-1,500*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
|
|
for (int i = 0; i < NV2A_PROF__COUNT; i++) {
|
|
ImGui::PushID(i);
|
|
char title[64];
|
|
snprintf(title, sizeof(title), "%s: %d",
|
|
nv2a_profile_get_counter_name(i),
|
|
nv2a_profile_get_counter_value(i));
|
|
ImPlot::PushStyleColor(ImPlotCol_Line, ImPlot::GetColormapColor(i));
|
|
ImPlot::PushStyleColor(ImPlotCol_Fill, ImPlot::GetColormapColor(i));
|
|
ImPlot::PlotLine(title, &g_nv2a_stats.frame_history[0].counters[i], NV2A_PROF_NUM_FRAMES, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
|
|
ImPlot::PopStyleColor(2);
|
|
ImGui::PopID();
|
|
}
|
|
ImPlot::EndPlot();
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(2)) {
|
|
transparent = !transparent;
|
|
}
|
|
|
|
ImPlot::PopStyleVar(2);
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleColor(5);
|
|
}
|
|
};
|
|
|
|
static MonitorWindow monitor_window;
|
|
static DebugApuWindow apu_window;
|
|
static DebugVideoWindow video_window;
|
|
static InputWindow input_window;
|
|
static NetworkWindow network_window;
|
|
static AboutWindow about_window;
|
|
static SettingsWindow settings_window;
|
|
static CompatibilityReporter compatibility_reporter_window;
|
|
static NotificationManager notification_manager;
|
|
static std::deque<const char *> g_errors;
|
|
|
|
class FirstBootWindow
|
|
{
|
|
public:
|
|
bool is_open;
|
|
|
|
FirstBootWindow()
|
|
{
|
|
is_open = false;
|
|
}
|
|
|
|
~FirstBootWindow()
|
|
{
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
if (!is_open) return;
|
|
|
|
ImVec2 size(400*g_ui_scale, 300*g_ui_scale);
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
ImVec2 window_pos = ImVec2((io.DisplaySize.x - size.x)/2, (io.DisplaySize.y - size.y)/2);
|
|
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always);
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Appearing);
|
|
if (!ImGui::Begin("First Boot", &is_open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDecoration)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
static uint32_t time_start = 0;
|
|
if (ImGui::IsWindowAppearing()) {
|
|
time_start = SDL_GetTicks();
|
|
}
|
|
uint32_t now = SDL_GetTicks() - time_start;
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50*g_ui_scale);
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_ui_scale)/2);
|
|
|
|
ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(logo_fbo);
|
|
float t_w = 256.0;
|
|
float t_h = 256.0;
|
|
float x_off = 0;
|
|
ImGui::Image(id,
|
|
ImVec2((t_w-x_off)*g_ui_scale, t_h*g_ui_scale),
|
|
ImVec2(x_off/t_w, t_h/t_h),
|
|
ImVec2(t_w/t_w, 0));
|
|
if (ImGui::IsItemClicked()) {
|
|
time_start = SDL_GetTicks();
|
|
}
|
|
render_logo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
|
|
render_to_default_fb();
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100*g_ui_scale);
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(xemu_version).x)/2);
|
|
ImGui::Text("%s", xemu_version);
|
|
|
|
ImGui::SetCursorPosX(10*g_ui_scale);
|
|
ImGui::Dummy(ImVec2(0,20*g_ui_scale));
|
|
|
|
const char *msg = "To get started, please configure machine settings.";
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
|
|
ImGui::Text("%s", msg);
|
|
|
|
ImGui::Dummy(ImVec2(0,20*g_ui_scale));
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_ui_scale)/2);
|
|
if (ImGui::Button("Settings", ImVec2(120*g_ui_scale, 0))) {
|
|
settings_window.is_open = true; // FIXME
|
|
}
|
|
ImGui::Dummy(ImVec2(0,20*g_ui_scale));
|
|
|
|
msg = "Visit https://xemu.app for more information";
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
|
|
ImGui::Text("%s", msg);
|
|
if (ImGui::IsItemClicked()) {
|
|
xemu_open_web_browser("https://xemu.app");
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
};
|
|
|
|
static bool is_shortcut_key_pressed(int scancode)
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
const bool is_osx = io.ConfigMacOSXBehaviors;
|
|
const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl
|
|
return is_shortcut_key && io.KeysDown[scancode] && (io.KeysDownDuration[scancode] == 0.0);
|
|
}
|
|
|
|
static void action_eject_disc(void)
|
|
{
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_DVD_PATH, "");
|
|
xemu_settings_save();
|
|
xemu_eject_disc();
|
|
}
|
|
|
|
static void action_load_disc(void)
|
|
{
|
|
const char *iso_file_filters = ".iso Files\0*.iso\0All Files\0*.*\0";
|
|
const char *current_disc_path;
|
|
xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_DVD_PATH, ¤t_disc_path);
|
|
const char *new_disc_path = paused_file_open(NOC_FILE_DIALOG_OPEN, iso_file_filters, current_disc_path, NULL);
|
|
if (new_disc_path == NULL) {
|
|
/* Cancelled */
|
|
return;
|
|
}
|
|
xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_DVD_PATH, new_disc_path);
|
|
xemu_settings_save();
|
|
xemu_load_disc(new_disc_path);
|
|
}
|
|
|
|
static void action_toggle_pause(void)
|
|
{
|
|
if (runstate_is_running()) {
|
|
vm_stop(RUN_STATE_PAUSED);
|
|
} else {
|
|
vm_start();
|
|
}
|
|
}
|
|
|
|
static void action_reset(void)
|
|
{
|
|
qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
|
|
}
|
|
|
|
static void action_shutdown(void)
|
|
{
|
|
qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI);
|
|
}
|
|
|
|
static void process_keyboard_shortcuts(void)
|
|
{
|
|
if (is_shortcut_key_pressed(SDL_SCANCODE_E)) {
|
|
action_eject_disc();
|
|
}
|
|
|
|
if (is_shortcut_key_pressed(SDL_SCANCODE_O)) {
|
|
action_load_disc();
|
|
}
|
|
|
|
if (is_shortcut_key_pressed(SDL_SCANCODE_P)) {
|
|
action_toggle_pause();
|
|
}
|
|
|
|
if (is_shortcut_key_pressed(SDL_SCANCODE_R)) {
|
|
action_reset();
|
|
}
|
|
|
|
if (is_shortcut_key_pressed(SDL_SCANCODE_Q)) {
|
|
action_shutdown();
|
|
}
|
|
}
|
|
|
|
#if defined(__APPLE__)
|
|
#define SHORTCUT_MENU_TEXT(c) "Cmd+" #c
|
|
#else
|
|
#define SHORTCUT_MENU_TEXT(c) "Ctrl+" #c
|
|
#endif
|
|
|
|
static void ShowMainMenu()
|
|
{
|
|
bool running = runstate_is_running();
|
|
|
|
if (ImGui::BeginMainMenuBar())
|
|
{
|
|
if (ImGui::BeginMenu("Machine"))
|
|
{
|
|
if (ImGui::MenuItem("Eject Disc", SHORTCUT_MENU_TEXT(E))) {
|
|
action_eject_disc();
|
|
}
|
|
if (ImGui::MenuItem("Load Disc...", SHORTCUT_MENU_TEXT(O))) {
|
|
action_load_disc();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
ImGui::MenuItem("Input", NULL, &input_window.is_open);
|
|
ImGui::MenuItem("Network", NULL, &network_window.is_open);
|
|
ImGui::MenuItem("Settings", NULL, &settings_window.is_open);
|
|
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::MenuItem(running ? "Pause" : "Run", SHORTCUT_MENU_TEXT(P))) {
|
|
action_toggle_pause();
|
|
}
|
|
if (ImGui::MenuItem("Reset", SHORTCUT_MENU_TEXT(R))) {
|
|
action_reset();
|
|
}
|
|
if (ImGui::MenuItem("Shutdown", SHORTCUT_MENU_TEXT(Q))) {
|
|
action_shutdown();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("View"))
|
|
{
|
|
int ui_scale_combo = g_ui_scale - 1.0;
|
|
if (ui_scale_combo < 0) ui_scale_combo = 0;
|
|
if (ui_scale_combo > 1) ui_scale_combo = 1;
|
|
if (ImGui::Combo("UI Scale", &ui_scale_combo, "1x\0" "2x\0")) {
|
|
g_ui_scale = ui_scale_combo + 1;
|
|
xemu_settings_set_int(XEMU_SETTINGS_DISPLAY_UI_SCALE, g_ui_scale);
|
|
xemu_settings_save();
|
|
g_trigger_style_update = true;
|
|
}
|
|
|
|
if (ImGui::Combo("Scaling Mode", &scaling_mode, "Center\0Scale\0Stretch\0")) {
|
|
xemu_settings_set_enum(XEMU_SETTINGS_DISPLAY_SCALE, scaling_mode);
|
|
xemu_settings_save();
|
|
}
|
|
ImGui::SameLine(); HelpMarker("Controls how the rendered content should be scaled into the window");
|
|
if (ImGui::MenuItem("Fullscreen", NULL, xemu_is_fullscreen(), true)) {
|
|
xemu_toggle_fullscreen();
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Debug"))
|
|
{
|
|
ImGui::MenuItem("Monitor", NULL, &monitor_window.is_open);
|
|
ImGui::MenuItem("Audio", NULL, &apu_window.is_open);
|
|
ImGui::MenuItem("Video", NULL, &video_window.is_open);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Help"))
|
|
{
|
|
ImGui::MenuItem("Report Compatibility", NULL, &compatibility_reporter_window.is_open);
|
|
ImGui::Separator();
|
|
ImGui::MenuItem("About", NULL, &about_window.is_open);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
g_main_menu_height = ImGui::GetWindowHeight();
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
}
|
|
|
|
static void InitializeStyle()
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
io.Fonts->Clear();
|
|
io.Fonts->AddFontFromFileTTF(xemu_get_resource_path("Roboto-Medium.ttf"), 16*g_ui_scale);
|
|
ImFontConfig font_cfg = ImFontConfig();
|
|
font_cfg.OversampleH = font_cfg.OversampleV = 1;
|
|
font_cfg.PixelSnapH = true;
|
|
font_cfg.SizePixels = 13.0f*g_ui_scale;
|
|
g_fixed_width_font = io.Fonts->AddFontDefault(&font_cfg);
|
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
|
|
|
ImGuiStyle style;
|
|
style.WindowRounding = 8.0;
|
|
style.FrameRounding = 8.0;
|
|
style.GrabRounding = 12.0;
|
|
style.PopupRounding = 5.0;
|
|
style.ScrollbarRounding = 12.0;
|
|
style.FramePadding.x = 10;
|
|
style.FramePadding.y = 4;
|
|
style.WindowBorderSize = 0;
|
|
style.PopupBorderSize = 0;
|
|
style.FrameBorderSize = 0;
|
|
style.TabBorderSize = 0;
|
|
ImGui::GetStyle() = style;
|
|
ImGui::GetStyle().ScaleAllSizes(g_ui_scale);
|
|
|
|
// Set default theme, override
|
|
ImGui::StyleColorsDark();
|
|
|
|
ImVec4* colors = ImGui::GetStyle().Colors;
|
|
colors[ImGuiCol_Text] = ImVec4(0.86f, 0.93f, 0.89f, 0.78f);
|
|
colors[ImGuiCol_TextDisabled] = ImVec4(0.86f, 0.93f, 0.89f, 0.28f);
|
|
colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.98f);
|
|
colors[ImGuiCol_ChildBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.58f);
|
|
colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.90f);
|
|
colors[ImGuiCol_Border] = ImVec4(0.11f, 0.11f, 0.11f, 0.60f);
|
|
colors[ImGuiCol_BorderShadow] = ImVec4(0.16f, 0.16f, 0.16f, 0.00f);
|
|
colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f);
|
|
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
|
|
colors[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.51f, 0.18f, 1.00f);
|
|
colors[ImGuiCol_TitleBgActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.16f, 0.16f, 0.16f, 0.75f);
|
|
colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.00f);
|
|
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f);
|
|
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.51f, 0.18f, 1.00f);
|
|
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
|
|
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.26f, 0.26f, 1.00f);
|
|
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_Button] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f);
|
|
colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_Header] = ImVec4(0.28f, 0.71f, 0.25f, 0.76f);
|
|
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.86f);
|
|
colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_Separator] = ImVec4(0.11f, 0.11f, 0.11f, 0.60f);
|
|
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.13f, 0.87f, 0.16f, 0.78f);
|
|
colors[ImGuiCol_SeparatorActive] = ImVec4(0.25f, 0.75f, 0.10f, 1.00f);
|
|
colors[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.83f, 0.49f, 0.04f);
|
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
|
|
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_Tab] = ImVec4(0.26f, 0.67f, 0.23f, 0.95f);
|
|
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.86f);
|
|
colors[ImGuiCol_TabActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
|
|
colors[ImGuiCol_TabUnfocused] = ImVec4(0.21f, 0.54f, 0.19f, 0.99f);
|
|
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.24f, 0.60f, 0.21f, 1.00f);
|
|
colors[ImGuiCol_PlotLines] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
|
|
colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_PlotHistogram] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
|
|
colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
|
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.28f, 0.71f, 0.25f, 0.43f);
|
|
colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
|
|
colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
|
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
|
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
|
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.73f);
|
|
}
|
|
|
|
/* External interface, called from ui/xemu.c which handles SDL main loop */
|
|
static FirstBootWindow first_boot_window;
|
|
static SDL_Window *g_sdl_window;
|
|
|
|
void xemu_hud_init(SDL_Window* window, void* sdl_gl_context)
|
|
{
|
|
xemu_monitor_init();
|
|
|
|
initialize_custom_ui_rendering();
|
|
|
|
// Setup Dear ImGui context
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
|
io.IniFilename = NULL;
|
|
|
|
// Setup Platform/Renderer bindings
|
|
ImGui_ImplSDL2_InitForOpenGL(window, sdl_gl_context);
|
|
ImGui_ImplOpenGL3_Init("#version 150");
|
|
|
|
first_boot_window.is_open = xemu_settings_did_fail_to_load();
|
|
|
|
int ui_scale_int = 1;
|
|
xemu_settings_get_int(XEMU_SETTINGS_DISPLAY_UI_SCALE, &ui_scale_int);
|
|
if (ui_scale_int < 1) ui_scale_int = 1;
|
|
g_ui_scale = ui_scale_int;
|
|
|
|
g_sdl_window = window;
|
|
|
|
ImPlot::CreateContext();
|
|
}
|
|
|
|
void xemu_hud_cleanup(void)
|
|
{
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplSDL2_Shutdown();
|
|
ImGui::DestroyContext();
|
|
}
|
|
|
|
void xemu_hud_process_sdl_events(SDL_Event *event)
|
|
{
|
|
ImGui_ImplSDL2_ProcessEvent(event);
|
|
}
|
|
|
|
void xemu_hud_should_capture_kbd_mouse(int *kbd, int *mouse)
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (kbd) *kbd = io.WantCaptureKeyboard;
|
|
if (mouse) *mouse = io.WantCaptureMouse;
|
|
}
|
|
|
|
void xemu_hud_render(void)
|
|
{
|
|
uint32_t now = SDL_GetTicks();
|
|
bool ui_wakeup = false;
|
|
|
|
// Combine all controller states to allow any controller to navigate
|
|
uint32_t buttons = 0;
|
|
int16_t axis[CONTROLLER_AXIS__COUNT] = {0};
|
|
|
|
ControllerState *iter;
|
|
QTAILQ_FOREACH(iter, &available_controllers, entry) {
|
|
if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue;
|
|
buttons |= iter->buttons;
|
|
// We simply take any axis that is >10 % activation
|
|
for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) {
|
|
if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) {
|
|
axis[i] = iter->axis[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the guide button is pressed, wake the ui
|
|
bool menu_button = false;
|
|
if (buttons & CONTROLLER_BUTTON_GUIDE) {
|
|
ui_wakeup = true;
|
|
menu_button = true;
|
|
}
|
|
|
|
// Allow controllers without a guide button to also work
|
|
if ((buttons & CONTROLLER_BUTTON_BACK) &&
|
|
(buttons & CONTROLLER_BUTTON_START)) {
|
|
ui_wakeup = true;
|
|
menu_button = true;
|
|
}
|
|
|
|
// If the mouse is moved, wake the ui
|
|
static ImVec2 last_mouse_pos = ImVec2();
|
|
ImVec2 current_mouse_pos = ImGui::GetMousePos();
|
|
if ((current_mouse_pos.x != last_mouse_pos.x) ||
|
|
(current_mouse_pos.y != last_mouse_pos.y)) {
|
|
last_mouse_pos = current_mouse_pos;
|
|
ui_wakeup = true;
|
|
}
|
|
|
|
// If mouse capturing is enabled (we are in a dialog), ensure the UI is alive
|
|
bool controller_focus_capture = false;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (io.NavActive) {
|
|
ui_wakeup = true;
|
|
controller_focus_capture = true;
|
|
}
|
|
|
|
// Prevent controller events from going to the guest if they are being used
|
|
// to navigate the HUD
|
|
xemu_input_set_test_mode(controller_focus_capture);
|
|
|
|
if (g_trigger_style_update) {
|
|
InitializeStyle();
|
|
g_trigger_style_update = false;
|
|
}
|
|
|
|
// Start the Dear ImGui frame
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
|
|
// Override SDL2 implementation gamecontroller interface
|
|
io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
|
|
ImGui_ImplSDL2_NewFrame(g_sdl_window);
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
|
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
|
|
|
// Update gamepad inputs (from imgui_impl_sdl.cpp)
|
|
memset(io.NavInputs, 0, sizeof(io.NavInputs));
|
|
#define MAP_BUTTON(NAV_NO, BUTTON_NO) { io.NavInputs[NAV_NO] = (buttons & BUTTON_NO) ? 1.0f : 0.0f; }
|
|
#define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float vn = (float)(axis[AXIS_NO] - V0) / (float)(V1 - V0); if (vn > 1.0f) vn = 1.0f; if (vn > 0.0f && io.NavInputs[NAV_NO] < vn) io.NavInputs[NAV_NO] = vn; }
|
|
const int thumb_dead_zone = 8000; // SDL_gamecontroller.h suggests using this value.
|
|
MAP_BUTTON(ImGuiNavInput_Activate, CONTROLLER_BUTTON_A); // Cross / A
|
|
MAP_BUTTON(ImGuiNavInput_Cancel, CONTROLLER_BUTTON_B); // Circle / B
|
|
MAP_BUTTON(ImGuiNavInput_Input, CONTROLLER_BUTTON_Y); // Triangle / Y
|
|
MAP_BUTTON(ImGuiNavInput_DpadLeft, CONTROLLER_BUTTON_DPAD_LEFT); // D-Pad Left
|
|
MAP_BUTTON(ImGuiNavInput_DpadRight, CONTROLLER_BUTTON_DPAD_RIGHT); // D-Pad Right
|
|
MAP_BUTTON(ImGuiNavInput_DpadUp, CONTROLLER_BUTTON_DPAD_UP); // D-Pad Up
|
|
MAP_BUTTON(ImGuiNavInput_DpadDown, CONTROLLER_BUTTON_DPAD_DOWN); // D-Pad Down
|
|
MAP_BUTTON(ImGuiNavInput_FocusPrev, CONTROLLER_BUTTON_WHITE); // L1 / LB
|
|
MAP_BUTTON(ImGuiNavInput_FocusNext, CONTROLLER_BUTTON_BLACK); // R1 / RB
|
|
MAP_BUTTON(ImGuiNavInput_TweakSlow, CONTROLLER_BUTTON_WHITE); // L1 / LB
|
|
MAP_BUTTON(ImGuiNavInput_TweakFast, CONTROLLER_BUTTON_BLACK); // R1 / RB
|
|
|
|
// Allow Guide and "Back+Start" buttons to act as Menu button
|
|
if (menu_button) {
|
|
io.NavInputs[ImGuiNavInput_Menu] = 1.0;
|
|
}
|
|
|
|
MAP_ANALOG(ImGuiNavInput_LStickLeft, CONTROLLER_AXIS_LSTICK_X, -thumb_dead_zone, -32768);
|
|
MAP_ANALOG(ImGuiNavInput_LStickRight, CONTROLLER_AXIS_LSTICK_X, +thumb_dead_zone, +32767);
|
|
MAP_ANALOG(ImGuiNavInput_LStickUp, CONTROLLER_AXIS_LSTICK_Y, +thumb_dead_zone, +32767);
|
|
MAP_ANALOG(ImGuiNavInput_LStickDown, CONTROLLER_AXIS_LSTICK_Y, -thumb_dead_zone, -32767);
|
|
|
|
ImGui::NewFrame();
|
|
process_keyboard_shortcuts();
|
|
|
|
bool show_main_menu = true;
|
|
|
|
if (first_boot_window.is_open) {
|
|
show_main_menu = false;
|
|
}
|
|
|
|
if (show_main_menu) {
|
|
// Auto-hide main menu after 5s of inactivity
|
|
static uint32_t last_check = 0;
|
|
float alpha = 1.0;
|
|
const uint32_t timeout = 5000;
|
|
const float fade_duration = 1000.0;
|
|
if (ui_wakeup) {
|
|
last_check = now;
|
|
}
|
|
if ((now-last_check) > timeout) {
|
|
float t = fmin((float)((now-last_check)-timeout)/fade_duration, 1.0);
|
|
alpha = 1.0-t;
|
|
if (t >= 1.0) {
|
|
alpha = 0.0;
|
|
}
|
|
}
|
|
if (alpha > 0.0) {
|
|
ImVec4 tc = ImGui::GetStyle().Colors[ImGuiCol_Text];
|
|
tc.w = alpha;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, tc);
|
|
ImGui::SetNextWindowBgAlpha(alpha);
|
|
ShowMainMenu();
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
g_main_menu_height = 0;
|
|
}
|
|
}
|
|
|
|
first_boot_window.Draw();
|
|
input_window.Draw();
|
|
settings_window.Draw();
|
|
monitor_window.Draw();
|
|
apu_window.Draw();
|
|
video_window.Draw();
|
|
about_window.Draw();
|
|
network_window.Draw();
|
|
compatibility_reporter_window.Draw();
|
|
notification_manager.Draw();
|
|
|
|
// Very rudimentary error notification API
|
|
if (g_errors.size() > 0) {
|
|
ImGui::OpenPopup("Error");
|
|
}
|
|
if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::Text("%s", g_errors[0]);
|
|
ImGui::Dummy(ImVec2(0,16));
|
|
ImGui::SetItemDefaultFocus();
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10));
|
|
if (ImGui::Button("Ok", ImVec2(120, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
free((void*)g_errors[0]);
|
|
g_errors.pop_front();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::Render();
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
}
|
|
|
|
/* External interface, exposed via xemu-notifications.h */
|
|
|
|
void xemu_queue_notification(const char *msg)
|
|
{
|
|
notification_manager.QueueNotification(msg);
|
|
}
|
|
|
|
void xemu_queue_error_message(const char *msg)
|
|
{
|
|
g_errors.push_back(strdup(msg));
|
|
}
|