From 334036d8246a40869c8875115eb061a8f6b7c449 Mon Sep 17 00:00:00 2001 From: MasaGratoR Date: Sun, 4 Aug 2024 12:13:12 +0200 Subject: [PATCH] Add "Game Resolutions" menu (#79) --- Makefile | 2 +- config/status-monitor/config.ini.template | 7 + docs/config.md | 11 ++ docs/modes.md | 30 +++- source/Utils.hpp | 103 ++++++++++++ source/main.cpp | 13 ++ source/modes/Resolutions.hpp | 182 ++++++++++++++++++++++ 7 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 source/modes/Resolutions.hpp diff --git a/Makefile b/Makefile index bfc8027b..11fd2622 100755 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ include $(DEVKITPRO)/libnx/switch_rules # NACP building is skipped as well. #--------------------------------------------------------------------------------- APP_TITLE := Status Monitor -APP_VERSION := 1.0.4 +APP_VERSION := 1.1.0 TARGET := $(notdir $(CURDIR)) BUILD := build SOURCES := source diff --git a/config/status-monitor/config.ini.template b/config/status-monitor/config.ini.template index ad2d52a0..b8f42bc8 100644 --- a/config/status-monitor/config.ini.template +++ b/config/status-monitor/config.ini.template @@ -51,3 +51,10 @@ dashed_line_color=#8888 main_line_color=#FFFF rounded_line_color=#F0FF perfect_line_color=#0C0F +[game_resolutions] +refresh_rate=10 +layer_width_align=left +layer_height_align=top +background_color=#1117 +cat_color=#FFFF +text_color=#FFFF \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index 1c444fec..63a2eefc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -85,3 +85,14 @@ Colors are provided in RGBA4444 format, which means that each character represen | `main_line_color` | Color of line representing FPS value on graph in RGBA4444 format | From `#0000` to `#FFFF` | `#FFFF` | | `rounded_line_color` | Color of line representing FPS value on graph if it's divisble by 10 in RGBA4444 format | From `#0000` to `#FFFF` | `#F0FF` | | `perfect_line_color` | Color of line representing FPS value on graph if it's divisble by 30 in RGBA4444 format | From `#0000` to `#FFFF` | `#0C0F` | + +> [game_resolutions] + +| Key | Explanation | Possible values | Default Value | +|-----|-------------|-----------------|---------------| +| `refresh_rate` | How often per second this mode should be refreshed. Higher value means higher CPU Core #3 usage, that's why it is recommended to stay at 1. | From `1` to `60` | `10` | +| `layer_width_align` | On which side of the screen X axis you want this mode | `left`, `center`, `right` | `left` | +| `layer_height_align` | On which side of the screen Y axis you want this mode | `top`, `center`, `bottom` | `top` | +| `background_color` | Background color in RGBA4444 format | From `#0000` to `#FFFF` | `#1117` | +| `cat_color` | Category text color (left side) in RGBA4444 format | From `#0000` to `#FFFF` | `#FFFF` | +| `text_color` | Stats text color (right side) in RGBA4444 format | From `#0000` to `#FFFF` | `#FFFF` | diff --git a/docs/modes.md b/docs/modes.md index b92bbec3..a0fec38b 100755 --- a/docs/modes.md +++ b/docs/modes.md @@ -1,6 +1,6 @@ # Modes -Status Monitor Overlay from 0.8 release contains six modes to choose from Main Menu.
+Status Monitor Overlay from 1.0.0 release contains five modes to choose from Main Menu.
For additional functions you need to install: - [SaltyNX](https://github.com/masagrator/SaltyNX/releases) - [sys-clk 2.0.0_rc4+](https://github.com/retronx-team/sys-clk/releases) (using closed source forks of sys-clk can result in retrieving wrong real clockrates and ram load) @@ -90,16 +90,16 @@ Contains most of supported informations with lower precision in one line. | FPS | %.1f | Frames Per Second | -# FPS Counter +# FPS + +> Counter It shows only FPS value in 31Hz + vsync signal.
If game is not launched, it will show always 254.0 value.
Mode available only with SaltyNX installed. -# Graphs - -> FPS +> Graph It shows average FPS graph in 31Hz + vsync signal. In background of graph you can see rendered actual average FPS.
If game is not launched, it will show always 254.0 value and graph will be empty.
@@ -145,6 +145,26 @@ Shows only if charger is connected: If Network Type is "Wi-Fi", you can press Y to show password. Since max password length is 64 characters, it may show in up to 3 lines. +> Game Resolutions + +For this mode to show and work properly you must have SaltyNX 0.9.0+ installed. + +When game runs, this menu shows what resolutions and how many times they were passed to GPU via two functions: +- __Depth__ shows info from depth texture passed to `nvnCommandBufferSetRenderTargets` +- __Viewport__ shows info from arguments passed to `nvnCommandBufferSetViewport` and `nvnCommandBufferSetViewports` + +This menu shows first 8 resolutions passed to those functions in last frame rendering loop, sorted in descending order of calls number.
+Its main purpose is to catch game rendering resolution, but user must deduce which ones are correct.
+I have limited catched resolutions only to ones that have ratio higher than 1.70 and lower than 1.90.
+ +Remember that resolutions you can see in this mode may be used in different ways - for example Tokyo Xanadu Ex+ max dynamic resolution in handheld will show 1280x736, but it's not that game will squeeze this into 720p screen, it's just removing those additional 16 pixels from showing on screen. + +Those commands are used by all 3D games using NVN API (that's why it won't work with other APIs and may not work with games using 2D engines).
+This mode is not 100% fullproof, so it can show that nothing is catched or it won't catch what is used for 3D rendering (if this happens for some 3D game, please report an issue). + +By default refresh rate of this menu is 10 FPS. You can change that in config.ini, more in config.md
+Exiting is done by using the same combo buttons used in other main modes. + # Additional info > How Battery Remaining Time is calculated diff --git a/source/Utils.hpp b/source/Utils.hpp index 8b4c90be..14696a65 100644 --- a/source/Utils.hpp +++ b/source/Utils.hpp @@ -142,11 +142,20 @@ SharedMemory _sharedmemory = {}; bool SharedMemoryUsed = false; uint32_t* MAGIC_shared = 0; uint8_t* FPS_shared = 0; +uint8_t* API_shared = 0; float* FPSavg_shared = 0; bool* pluginActive = 0; uint32_t* FPSticks_shared = 0; Handle remoteSharedMemory = 1; +struct resolutionCalls { + uint16_t width; + uint16_t height; + uint16_t calls; +}; +resolutionCalls* renderCalls_shared = 0; +resolutionCalls* viewportCalls_shared = 0; + //Read real freqs from sys-clk sysmodule uint32_t realCPU_Hz = 0; uint32_t realGPU_Hz = 0; @@ -154,6 +163,11 @@ uint32_t realRAM_Hz = 0; uint32_t ramLoad[SysClkRamLoad_EnumMax]; uint8_t refreshRate = 0; +int compare (const void* elem1, const void* elem2) { + if ((((resolutionCalls*)(elem1)) -> calls) > (((resolutionCalls*)(elem2)) -> calls)) return -1; + else return 1; +} + void LoadSharedMemoryAndRefreshRate() { if (SaltySD_Connect()) return; @@ -237,7 +251,10 @@ void CheckIfGameRunning(void*) { FPS_shared = (uint8_t*)(base + rel_offset + 4); FPSavg_shared = (float*)(base + rel_offset + 5); pluginActive = (bool*)(base + rel_offset + 9); + API_shared = (uint8_t*)(base + rel_offset + 14); FPSticks_shared = (uint32_t*)(base + rel_offset + 15); + renderCalls_shared = (resolutionCalls*)(base + rel_offset + 60); + viewportCalls_shared = (resolutionCalls*)(base + rel_offset + 60 + (sizeof(resolutionCalls) * 8)); *pluginActive = false; svcSleepThread(100'000'000); if (*pluginActive) { @@ -941,6 +958,14 @@ struct FpsGraphSettings { int setPos; }; +struct ResolutionSettings { + uint8_t refreshRate; + uint16_t backgroundColor; + uint16_t catColor; + uint16_t textColor; + int setPos; +}; + void GetConfigSettings(MiniSettings* settings) { settings -> realFrequencies = false; settings -> handheldFontSize = 15; @@ -951,6 +976,7 @@ void GetConfigSettings(MiniSettings* settings) { settings -> show = "CPU+GPU+RAM+TEMP+DRAW+FAN+FPS"; settings -> showRAMLoad = true; settings -> refreshRate = 1; + settings -> setPos = 0; FILE* configFileIn = fopen("sdmc:/config/status-monitor/config.ini", "r"); if (!configFileIn) @@ -1412,3 +1438,80 @@ void GetConfigSettings(FullSettings* settings) { settings -> showTargetFreqs = key.compare("FALSE"); } } + +void GetConfigSettings(ResolutionSettings* settings) { + convertStrToRGBA4444("#1117", &(settings -> backgroundColor)); + convertStrToRGBA4444("#FFFF", &(settings -> catColor)); + convertStrToRGBA4444("#FFFF", &(settings -> textColor)); + settings -> refreshRate = 10; + settings -> setPos = 0; + + FILE* configFileIn = fopen("sdmc:/config/status-monitor/config.ini", "r"); + if (!configFileIn) + return; + fseek(configFileIn, 0, SEEK_END); + long fileSize = ftell(configFileIn); + rewind(configFileIn); + + std::string fileDataString(fileSize, '\0'); + fread(&fileDataString[0], sizeof(char), fileSize, configFileIn); + fclose(configFileIn); + + auto parsedData = tsl::hlp::ini::parseIni(fileDataString); + + std::string key; + if (parsedData.find("game_resolutions") == parsedData.end()) + return; + if (parsedData["game_resolutions"].find("refresh_rate") != parsedData["game_resolutions"].end()) { + long maxFPS = 60; + long minFPS = 1; + + key = parsedData["game_resolutions"]["refresh_rate"]; + long rate = atol(key.c_str()); + if (rate < minFPS) { + settings -> refreshRate = minFPS; + } + else if (rate > maxFPS) + settings -> refreshRate = maxFPS; + else settings -> refreshRate = rate; + } + + if (parsedData["game_resolutions"].find("background_color") != parsedData["game_resolutions"].end()) { + key = parsedData["game_resolutions"]["background_color"]; + uint16_t temp = 0; + if (convertStrToRGBA4444(key, &temp)) + settings -> backgroundColor = temp; + } + if (parsedData["game_resolutions"].find("cat_color") != parsedData["game_resolutions"].end()) { + key = parsedData["game_resolutions"]["cat_color"]; + uint16_t temp = 0; + if (convertStrToRGBA4444(key, &temp)) + settings -> catColor = temp; + } + if (parsedData["game_resolutions"].find("text_color") != parsedData["game_resolutions"].end()) { + key = parsedData["game_resolutions"]["text_color"]; + uint16_t temp = 0; + if (convertStrToRGBA4444(key, &temp)) + settings -> textColor = temp; + } + if (parsedData["game_resolutions"].find("layer_width_align") != parsedData["game_resolutions"].end()) { + key = parsedData["game_resolutions"]["layer_width_align"]; + convertToUpper(key); + if (!key.compare("CENTER")) { + settings -> setPos = 1; + } + if (!key.compare("RIGHT")) { + settings -> setPos = 2; + } + } + if (parsedData["game_resolutions"].find("layer_height_align") != parsedData["game_resolutions"].end()) { + key = parsedData["game_resolutions"]["layer_height_align"]; + convertToUpper(key); + if (!key.compare("CENTER")) { + settings -> setPos += 3; + } + if (!key.compare("BOTTOM")) { + settings -> setPos += 6; + } + } +} \ No newline at end of file diff --git a/source/main.cpp b/source/main.cpp index c9466f0e..269faac5 100755 --- a/source/main.cpp +++ b/source/main.cpp @@ -13,6 +13,7 @@ static bool skipMain = false; #include "modes/Micro.hpp" #include "modes/Battery.hpp" #include "modes/Misc.hpp" +#include "modes/Resolutions.hpp" //Graphs class GraphsMenu : public tsl::Gui { @@ -88,6 +89,18 @@ class OtherMenu : public tsl::Gui { }); list->addItem(Misc); + if (SaltySD) { + auto Res = new tsl::elm::ListItem("Game Resolutions"); + Res->setClickListener([](uint64_t keys) { + if (keys & KEY_A) { + tsl::changeTo(); + return true; + } + return false; + }); + list->addItem(Res); + } + rootFrame->setContent(list); return rootFrame; diff --git a/source/modes/Resolutions.hpp b/source/modes/Resolutions.hpp new file mode 100644 index 00000000..98901ad5 --- /dev/null +++ b/source/modes/Resolutions.hpp @@ -0,0 +1,182 @@ +class ResolutionsOverlay : public tsl::Gui { +private: + uint64_t mappedButtons = MapButtons(keyCombo); + char Resolutions_c[512]; + char Resolutions2_c[512]; + ResolutionSettings settings; +public: + ResolutionsOverlay() { + GetConfigSettings(&settings); + switch(settings.setPos) { + case 1: + case 4: + case 7: + tsl::gfx::Renderer::getRenderer().setLayerPos(639, 0); + break; + case 2: + case 5: + case 8: + tsl::gfx::Renderer::getRenderer().setLayerPos(1248, 0); + break; + } + StartFPSCounterThread(); + deactivateOriginalFooter = true; + tsl::hlp::requestForeground(false); + alphabackground = 0x0; + FullMode = false; + TeslaFPS = settings.refreshRate; + } + ~ResolutionsOverlay() { + TeslaFPS = 60; + EndFPSCounterThread(); + deactivateOriginalFooter = false; + tsl::hlp::requestForeground(true); + alphabackground = 0xD; + FullMode = true; + if (settings.setPos) + tsl::gfx::Renderer::getRenderer().setLayerPos(0, 0); + } + + resolutionCalls m_resolutionRenderCalls[8] = {0}; + resolutionCalls m_resolutionViewportCalls[8] = {0}; + bool gameStart = false; + uint8_t resolutionLookup = 0; + + virtual tsl::elm::Element* createUI() override { + rootFrame = new tsl::elm::OverlayFrame("", ""); + + auto Status = new tsl::elm::CustomDrawer([this](tsl::gfx::Renderer *renderer, u16 x, u16 y, u16 w, u16 h) { + int base_y = 0; + int base_x = 0; + switch(settings.setPos) { + case 1: + base_x = 48; + break; + case 2: + base_x = 96; + break; + case 3: + base_y = 260; + break; + case 4: + base_x = 48; + base_y = 260; + break; + case 5: + base_x = 96; + base_y = 260; + break; + case 6: + base_y = 520; + break; + case 7: + base_x = 48; + base_y = 520; + break; + case 8: + base_x = 96; + base_y = 520; + break; + } + + if (gameStart && *API_shared == 1) { + renderer->drawRect(base_x, base_y, 360, 200, a(settings.backgroundColor)); + + renderer->drawString("Depth:", false, base_x+20, base_y+20, 20, renderer->a(settings.catColor)); + renderer->drawString(Resolutions_c, false, base_x+20, base_y+55, 18, renderer->a(settings.textColor)); + renderer->drawString("Viewport:", false, base_x+180, base_y+20, 20, renderer->a(settings.catColor)); + renderer->drawString(Resolutions2_c, false, base_x+180, base_y+55, 18, renderer->a(settings.textColor)); + } + else { + switch(settings.setPos) { + case 3 ... 5: + base_y = 346; + break; + case 6 ... 8: + base_y = 692; + break; + } + if (gameStart && *API_shared > 1) { + renderer->drawRect(base_x, base_y, 360, 28, a(settings.backgroundColor)); + renderer->drawString("Game doesn't use NVN, it's incompatible.", false, base_x, base_y+20, 18, renderer->a(0xF00F)); + } + else { + renderer->drawRect(base_x, base_y, 360, 28, a(settings.backgroundColor)); + renderer->drawString("Game is not running or it's incompatible.", false, base_x, base_y+20, 18, renderer->a(0xF00F)); + } + } + }); + + rootFrame->setContent(Status); + + return rootFrame; + } + + virtual void update() override { + + if (gameStart && renderCalls_shared) { + if (!resolutionLookup) { + renderCalls_shared[0].calls = 0xFFFF; + resolutionLookup = 1; + } + else if (resolutionLookup == 1) { + if (renderCalls_shared[0].calls != 0xFFFF) resolutionLookup = 2; + else return; + } + memcpy(&m_resolutionRenderCalls, renderCalls_shared, sizeof(m_resolutionRenderCalls)); + memcpy(&m_resolutionViewportCalls, viewportCalls_shared, sizeof(m_resolutionViewportCalls)); + qsort(m_resolutionRenderCalls, 8, sizeof(resolutionCalls), compare); + qsort(m_resolutionViewportCalls, 8, sizeof(resolutionCalls), compare); + snprintf(Resolutions_c, sizeof Resolutions_c, + "1. %dx%d, %d\n" + "2. %dx%d, %d\n" + "3. %dx%d, %d\n" + "4. %dx%d, %d\n" + "5. %dx%d, %d\n" + "6. %dx%d, %d\n" + "7. %dx%d, %d\n" + "8. %dx%d, %d", + m_resolutionRenderCalls[0].width, m_resolutionRenderCalls[0].height, m_resolutionRenderCalls[0].calls, + m_resolutionRenderCalls[1].width, m_resolutionRenderCalls[1].height, m_resolutionRenderCalls[1].calls, + m_resolutionRenderCalls[2].width, m_resolutionRenderCalls[2].height, m_resolutionRenderCalls[2].calls, + m_resolutionRenderCalls[3].width, m_resolutionRenderCalls[3].height, m_resolutionRenderCalls[3].calls, + m_resolutionRenderCalls[4].width, m_resolutionRenderCalls[4].height, m_resolutionRenderCalls[4].calls, + m_resolutionRenderCalls[5].width, m_resolutionRenderCalls[5].height, m_resolutionRenderCalls[5].calls, + m_resolutionRenderCalls[6].width, m_resolutionRenderCalls[6].height, m_resolutionRenderCalls[6].calls, + m_resolutionRenderCalls[7].width, m_resolutionRenderCalls[7].height, m_resolutionRenderCalls[7].calls + ); + snprintf(Resolutions2_c, sizeof Resolutions2_c, + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d\n" + "%dx%d, %d", + m_resolutionViewportCalls[0].width, m_resolutionViewportCalls[0].height, m_resolutionViewportCalls[0].calls, + m_resolutionViewportCalls[1].width, m_resolutionViewportCalls[1].height, m_resolutionViewportCalls[1].calls, + m_resolutionViewportCalls[2].width, m_resolutionViewportCalls[2].height, m_resolutionViewportCalls[2].calls, + m_resolutionViewportCalls[3].width, m_resolutionViewportCalls[3].height, m_resolutionViewportCalls[3].calls, + m_resolutionViewportCalls[4].width, m_resolutionViewportCalls[4].height, m_resolutionViewportCalls[4].calls, + m_resolutionViewportCalls[5].width, m_resolutionViewportCalls[5].height, m_resolutionViewportCalls[5].calls, + m_resolutionViewportCalls[6].width, m_resolutionViewportCalls[6].height, m_resolutionViewportCalls[6].calls, + m_resolutionViewportCalls[7].width, m_resolutionViewportCalls[7].height, m_resolutionViewportCalls[7].calls + ); + } + if (FPSavg < 254) { + gameStart = true; + } + else { + gameStart = false; + resolutionLookup = false; + } + } + virtual bool handleInput(uint64_t keysDown, uint64_t keysHeld, touchPosition touchInput, JoystickPosition leftJoyStick, JoystickPosition rightJoyStick) override { + if (isKeyComboPressed(keysHeld, keysDown, mappedButtons)) { + tsl::goBack(); + return true; + } + return false; + } +};