1 // Copyright (c) 2012- PPSSPP Project.
2
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, version 2.0 or later versions.
6
7 // This program is distributed in the hope that it will be useful,
8 // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // GNU General Public License 2.0 for more details.
11
12 // A copy of the GPL 2.0 should have been included with the program.
13 // If not, see http://www.gnu.org/licenses/
14
15 // Official git repository and contact information can be found at
16 // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17
18 #include "ppsspp_config.h"
19
20 #include <algorithm>
21 #include <functional>
22
23 using namespace std::placeholders;
24
25 #include "Common/Render/TextureAtlas.h"
26 #include "Common/GPU/OpenGL/GLFeatures.h"
27 #include "Common/Render/Text/draw_text.h"
28
29 #include "Common/UI/Root.h"
30 #include "Common/UI/UI.h"
31 #include "Common/UI/Context.h"
32 #include "Common/UI/Tween.h"
33 #include "Common/UI/View.h"
34
35 #include "Common/Data/Text/I18n.h"
36 #include "Common/Input/InputState.h"
37 #include "Common/Log.h"
38 #include "Common/System/Display.h"
39 #include "Common/System/System.h"
40 #include "Common/System/NativeApp.h"
41 #include "Common/Profiler/Profiler.h"
42 #include "Common/Math/curves.h"
43 #include "Common/TimeUtil.h"
44
45 #ifndef MOBILE_DEVICE
46 #include "Core/AVIDump.h"
47 #endif
48 #include "Core/Config.h"
49 #include "Core/ConfigValues.h"
50 #include "Core/CoreTiming.h"
51 #include "Core/CoreParameter.h"
52 #include "Core/Core.h"
53 #include "Core/CwCheat.h"
54 #include "Core/Host.h"
55 #include "Core/KeyMap.h"
56 #include "Core/MemFault.h"
57 #include "Core/Reporting.h"
58 #include "Core/System.h"
59 #include "GPU/GPUState.h"
60 #include "GPU/GPUInterface.h"
61 #include "GPU/Common/FramebufferManagerCommon.h"
62 #if !PPSSPP_PLATFORM(UWP)
63 #include "GPU/Vulkan/DebugVisVulkan.h"
64 #endif
65 #include "Core/HLE/sceCtrl.h"
66 #include "Core/HLE/sceDisplay.h"
67 #include "Core/HLE/sceSas.h"
68 #include "Core/Debugger/SymbolMap.h"
69 #include "Core/SaveState.h"
70 #include "Core/MIPS/MIPS.h"
71 #include "Core/HLE/__sceAudio.h"
72 #include "Core/HLE/proAdhoc.h"
73 #include "Core/HLE/Plugins.h"
74
75 #include "UI/BackgroundAudio.h"
76 #include "UI/OnScreenDisplay.h"
77 #include "UI/GamepadEmu.h"
78 #include "UI/PauseScreen.h"
79 #include "UI/MainScreen.h"
80 #include "UI/EmuScreen.h"
81 #include "UI/DevScreens.h"
82 #include "UI/GameInfoCache.h"
83 #include "UI/MiscScreens.h"
84 #include "UI/ControlMappingScreen.h"
85 #include "UI/DisplayLayoutScreen.h"
86 #include "UI/GameSettingsScreen.h"
87 #include "UI/ProfilerDraw.h"
88 #include "UI/DiscordIntegration.h"
89 #include "UI/ChatScreen.h"
90
91 #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
92 #include "Windows/MainWindow.h"
93 #endif
94
95 #ifndef MOBILE_DEVICE
96 static AVIDump avi;
97 #endif
98
99 // TODO: Ugly!
100 static bool frameStep_;
101 static int lastNumFlips;
102 static bool startDumping;
103
104 extern bool g_TakeScreenshot;
105
__EmuScreenVblank()106 static void __EmuScreenVblank()
107 {
108 auto sy = GetI18NCategory("System");
109
110 if (frameStep_ && lastNumFlips != gpuStats.numFlips)
111 {
112 frameStep_ = false;
113 Core_EnableStepping(true);
114 lastNumFlips = gpuStats.numFlips;
115 }
116 #ifndef MOBILE_DEVICE
117 if (g_Config.bDumpFrames && !startDumping)
118 {
119 avi.Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
120 osm.Show(sy->T("AVI Dump started."), 0.5f);
121 startDumping = true;
122 }
123 if (g_Config.bDumpFrames && startDumping)
124 {
125 avi.AddFrame();
126 }
127 else if (!g_Config.bDumpFrames && startDumping)
128 {
129 avi.Stop();
130 osm.Show(sy->T("AVI Dump stopped."), 1.0f);
131 startDumping = false;
132 }
133 #endif
134 }
135
136 // Handles control rotation due to internal screen rotation.
137 // TODO: This should be a callback too, so we don't actually call the __Ctrl functions
138 // from settings screens, etc.
SetPSPAnalog(int stick,float x,float y)139 static void SetPSPAnalog(int stick, float x, float y) {
140 switch (g_Config.iInternalScreenRotation) {
141 case ROTATION_LOCKED_HORIZONTAL:
142 // Standard rotation. No change.
143 break;
144 case ROTATION_LOCKED_HORIZONTAL180:
145 x = -x;
146 y = -y;
147 break;
148 case ROTATION_LOCKED_VERTICAL:
149 {
150 float new_y = -x;
151 x = y;
152 y = new_y;
153 break;
154 }
155 case ROTATION_LOCKED_VERTICAL180:
156 {
157 float new_y = y = x;
158 x = -y;
159 y = new_y;
160 break;
161 }
162 default:
163 break;
164 }
165
166 __CtrlSetAnalogXY(stick, x, y);
167 }
168
EmuScreen(const Path & filename)169 EmuScreen::EmuScreen(const Path &filename)
170 : gamePath_(filename) {
171 saveStateSlot_ = SaveState::GetCurrentSlot();
172 __DisplayListenVblank(__EmuScreenVblank);
173 frameStep_ = false;
174 lastNumFlips = gpuStats.numFlips;
175 startDumping = false;
176 controlMapper_.SetCallbacks(
177 std::bind(&EmuScreen::onVKeyDown, this, _1),
178 std::bind(&EmuScreen::onVKeyUp, this, _1),
179 &SetPSPAnalog);
180
181 // Make sure we don't leave it at powerdown after the last game.
182 // TODO: This really should be handled elsewhere if it isn't.
183 if (coreState == CORE_POWERDOWN)
184 coreState = CORE_STEPPING;
185
186 OnDevMenu.Handle(this, &EmuScreen::OnDevTools);
187 OnChatMenu.Handle(this, &EmuScreen::OnChat);
188
189 // Usually, we don't want focus movement enabled on this screen, so disable on start.
190 // Only if you open chat or dev tools do we want it to start working.
191 UI::EnableFocusMovement(false);
192 }
193
bootAllowStorage(const Path & filename)194 bool EmuScreen::bootAllowStorage(const Path &filename) {
195 // No permissions needed. The easy life.
196 if (filename.Type() == PathType::HTTP)
197 return true;
198
199 if (!System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS))
200 return true;
201
202 PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE);
203 switch (status) {
204 case PERMISSION_STATUS_UNKNOWN:
205 System_AskForPermission(SYSTEM_PERMISSION_STORAGE);
206 return false;
207
208 case PERMISSION_STATUS_DENIED:
209 stopRender_ = true;
210 screenManager()->switchScreen(new MainScreen());
211 System_SendMessage("event", "failstartgame");
212 return false;
213
214 case PERMISSION_STATUS_PENDING:
215 // Keep waiting.
216 return false;
217
218 case PERMISSION_STATUS_GRANTED:
219 return true;
220 }
221
222 _assert_(false);
223 return false;
224 }
225
bootGame(const Path & filename)226 void EmuScreen::bootGame(const Path &filename) {
227 if (PSP_IsIniting()) {
228 std::string error_string;
229 bootPending_ = !PSP_InitUpdate(&error_string);
230 if (!bootPending_) {
231 invalid_ = !PSP_IsInited();
232 if (invalid_) {
233 errorMessage_ = error_string;
234 ERROR_LOG(BOOT, "%s", errorMessage_.c_str());
235 System_SendMessage("event", "failstartgame");
236 return;
237 }
238 bootComplete();
239 }
240 return;
241 }
242
243 g_BackgroundAudio.SetGame(Path());
244
245 // Check permission status first, in case we came from a shortcut.
246 if (!bootAllowStorage(filename))
247 return;
248
249 auto sc = GetI18NCategory("Screen");
250
251 invalid_ = true;
252
253 // We don't want to boot with the wrong game specific config, so wait until info is ready.
254 std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(nullptr, filename, 0);
255 if (!info || info->pending)
256 return;
257
258 if (!info->id.empty()) {
259 g_Config.loadGameConfig(info->id, info->GetTitle());
260 // Reset views in case controls are in a different place.
261 RecreateViews();
262
263 g_Discord.SetPresenceGame(info->GetTitle().c_str());
264 } else {
265 g_Discord.SetPresenceGame(sc->T("Untitled PSP game"));
266 }
267
268 CoreParameter coreParam{};
269 coreParam.cpuCore = (CPUCore)g_Config.iCpuCore;
270 coreParam.gpuCore = GPUCORE_GLES;
271 switch (GetGPUBackend()) {
272 case GPUBackend::DIRECT3D11:
273 coreParam.gpuCore = GPUCORE_DIRECTX11;
274 break;
275 #if !PPSSPP_PLATFORM(UWP)
276 #if PPSSPP_API(ANY_GL)
277 case GPUBackend::OPENGL:
278 coreParam.gpuCore = GPUCORE_GLES;
279 break;
280 #endif
281 case GPUBackend::DIRECT3D9:
282 coreParam.gpuCore = GPUCORE_DIRECTX9;
283 break;
284 case GPUBackend::VULKAN:
285 coreParam.gpuCore = GPUCORE_VULKAN;
286 break;
287 #endif
288 }
289
290 // Preserve the existing graphics context.
291 coreParam.graphicsContext = PSP_CoreParameter().graphicsContext;
292 coreParam.enableSound = g_Config.bEnableSound;
293 coreParam.fileToStart = filename;
294 coreParam.mountIso.clear();
295 coreParam.mountRoot.clear();
296 coreParam.startBreak = !g_Config.bAutoRun;
297 coreParam.printfEmuLog = false;
298 coreParam.headLess = false;
299
300 if (g_Config.iInternalResolution == 0) {
301 coreParam.renderWidth = pixel_xres;
302 coreParam.renderHeight = pixel_yres;
303 } else {
304 if (g_Config.iInternalResolution < 0)
305 g_Config.iInternalResolution = 1;
306 coreParam.renderWidth = 480 * g_Config.iInternalResolution;
307 coreParam.renderHeight = 272 * g_Config.iInternalResolution;
308 }
309 coreParam.pixelWidth = pixel_xres;
310 coreParam.pixelHeight = pixel_yres;
311
312 std::string error_string;
313 if (!PSP_InitStart(coreParam, &error_string)) {
314 bootPending_ = false;
315 invalid_ = true;
316 errorMessage_ = error_string;
317 ERROR_LOG(BOOT, "%s", errorMessage_.c_str());
318 System_SendMessage("event", "failstartgame");
319 }
320
321 if (PSP_CoreParameter().compat.flags().RequireBufferedRendering && g_Config.iRenderingMode == FB_NON_BUFFERED_MODE) {
322 auto gr = GetI18NCategory("Graphics");
323 host->NotifyUserMessage(gr->T("BufferedRenderingRequired", "Warning: This game requires Rendering Mode to be set to Buffered."), 15.0f);
324 }
325
326 if (PSP_CoreParameter().compat.flags().RequireBlockTransfer && g_Config.bBlockTransferGPU == false) {
327 auto gr = GetI18NCategory("Graphics");
328 host->NotifyUserMessage(gr->T("BlockTransferRequired", "Warning: This game requires Simulate Block Transfer Mode to be set to On."), 15.0f);
329 }
330
331 if (PSP_CoreParameter().compat.flags().RequireDefaultCPUClock && g_Config.iLockedCPUSpeed != 0) {
332 auto gr = GetI18NCategory("Graphics");
333 host->NotifyUserMessage(gr->T("DefaultCPUClockRequired", "Warning: This game requires the CPU clock to be set to default."), 15.0f);
334 }
335
336 loadingViewColor_->Divert(0xFFFFFFFF, 0.75f);
337 loadingViewVisible_->Divert(UI::V_VISIBLE, 0.75f);
338 }
339
bootComplete()340 void EmuScreen::bootComplete() {
341 UpdateUIState(UISTATE_INGAME);
342 host->BootDone();
343 host->UpdateDisassembly();
344
345 NOTICE_LOG(BOOT, "Loading %s...", PSP_CoreParameter().fileToStart.c_str());
346 autoLoad();
347
348 auto sc = GetI18NCategory("Screen");
349
350 #ifndef MOBILE_DEVICE
351 if (g_Config.bFirstRun) {
352 osm.Show(sc->T("PressESC", "Press ESC to open the pause menu"), 3.0f);
353 }
354 #endif
355
356 #if !PPSSPP_PLATFORM(UWP)
357 if (GetGPUBackend() == GPUBackend::OPENGL) {
358 const char *renderer = gl_extensions.model;
359 if (strstr(renderer, "Chainfire3D") != 0) {
360 osm.Show(sc->T("Chainfire3DWarning", "WARNING: Chainfire3D detected, may cause problems"), 10.0f, 0xFF30a0FF, -1, true);
361 } else if (strstr(renderer, "GLTools") != 0) {
362 osm.Show(sc->T("GLToolsWarning", "WARNING: GLTools detected, may cause problems"), 10.0f, 0xFF30a0FF, -1, true);
363 }
364
365 if (g_Config.bGfxDebugOutput) {
366 osm.Show("WARNING: GfxDebugOutput is enabled via ppsspp.ini. Things may be slow.", 10.0f, 0xFF30a0FF, -1, true);
367 }
368 }
369 #endif
370
371 if (Core_GetPowerSaving()) {
372 auto sy = GetI18NCategory("System");
373 #ifdef __ANDROID__
374 osm.Show(sy->T("WARNING: Android battery save mode is on"), 2.0f, 0xFFFFFF, -1, true, "core_powerSaving");
375 #else
376 osm.Show(sy->T("WARNING: Battery save mode is on"), 2.0f, 0xFFFFFF, -1, true, "core_powerSaving");
377 #endif
378 }
379
380 System_SendMessage("event", "startgame");
381
382 saveStateSlot_ = SaveState::GetCurrentSlot();
383
384 loadingViewColor_->Divert(0x00FFFFFF, 0.2f);
385 loadingViewVisible_->Divert(UI::V_INVISIBLE, 0.2f);
386 }
387
~EmuScreen()388 EmuScreen::~EmuScreen() {
389 if (!invalid_ || bootPending_) {
390 // If we were invalid, it would already be shutdown.
391 PSP_Shutdown();
392 }
393 #ifndef MOBILE_DEVICE
394 if (g_Config.bDumpFrames && startDumping)
395 {
396 avi.Stop();
397 osm.Show("AVI Dump stopped.", 1.0f);
398 startDumping = false;
399 }
400 #endif
401
402 if (GetUIState() == UISTATE_EXIT)
403 g_Discord.ClearPresence();
404 else
405 g_Discord.SetPresenceMenu();
406 }
407
dialogFinished(const Screen * dialog,DialogResult result)408 void EmuScreen::dialogFinished(const Screen *dialog, DialogResult result) {
409 // TODO: improve the way with which we got commands from PauseMenu.
410 // DR_CANCEL/DR_BACK means clicked on "continue", DR_OK means clicked on "back to menu",
411 // DR_YES means a message sent to PauseMenu by NativeMessageReceived.
412 if (result == DR_OK || quit_) {
413 screenManager()->switchScreen(new MainScreen());
414 System_SendMessage("event", "exitgame");
415 quit_ = false;
416 }
417 RecreateViews();
418 }
419
AfterSaveStateAction(SaveState::Status status,const std::string & message,void *)420 static void AfterSaveStateAction(SaveState::Status status, const std::string &message, void *) {
421 if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) {
422 osm.Show(message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0);
423 }
424 }
425
AfterStateBoot(SaveState::Status status,const std::string & message,void * ignored)426 static void AfterStateBoot(SaveState::Status status, const std::string &message, void *ignored) {
427 AfterSaveStateAction(status, message, ignored);
428 Core_EnableStepping(false);
429 host->UpdateDisassembly();
430 }
431
sendMessage(const char * message,const char * value)432 void EmuScreen::sendMessage(const char *message, const char *value) {
433 // External commands, like from the Windows UI.
434 if (!strcmp(message, "pause") && screenManager()->topScreen() == this) {
435 screenManager()->push(new GamePauseScreen(gamePath_));
436 } else if (!strcmp(message, "stop")) {
437 // We will push MainScreen in update().
438 PSP_Shutdown();
439 bootPending_ = false;
440 stopRender_ = true;
441 invalid_ = true;
442 host->UpdateDisassembly();
443 } else if (!strcmp(message, "reset")) {
444 PSP_Shutdown();
445 bootPending_ = true;
446 invalid_ = true;
447 host->UpdateDisassembly();
448
449 std::string resetError;
450 if (!PSP_InitStart(PSP_CoreParameter(), &resetError)) {
451 ERROR_LOG(LOADER, "Error resetting: %s", resetError.c_str());
452 stopRender_ = true;
453 screenManager()->switchScreen(new MainScreen());
454 System_SendMessage("event", "failstartgame");
455 return;
456 }
457 } else if (!strcmp(message, "boot")) {
458 const char *ext = strrchr(value, '.');
459 if (ext != nullptr && !strcmp(ext, ".ppst")) {
460 SaveState::Load(Path(value), -1, &AfterStateBoot);
461 } else {
462 PSP_Shutdown();
463 bootPending_ = true;
464 gamePath_ = Path(value);
465 // Don't leave it on CORE_POWERDOWN, we'll sometimes aggressively bail.
466 Core_UpdateState(CORE_POWERUP);
467 }
468 } else if (!strcmp(message, "config_loaded")) {
469 // In case we need to position touch controls differently.
470 RecreateViews();
471 } else if (!strcmp(message, "control mapping") && screenManager()->topScreen() == this) {
472 UpdateUIState(UISTATE_PAUSEMENU);
473 screenManager()->push(new ControlMappingScreen());
474 } else if (!strcmp(message, "display layout editor") && screenManager()->topScreen() == this) {
475 UpdateUIState(UISTATE_PAUSEMENU);
476 screenManager()->push(new DisplayLayoutScreen());
477 } else if (!strcmp(message, "settings") && screenManager()->topScreen() == this) {
478 UpdateUIState(UISTATE_PAUSEMENU);
479 screenManager()->push(new GameSettingsScreen(gamePath_));
480 } else if (!strcmp(message, "gpu dump next frame")) {
481 if (gpu)
482 gpu->DumpNextFrame();
483 } else if (!strcmp(message, "clear jit")) {
484 currentMIPS->ClearJitCache();
485 if (PSP_IsInited()) {
486 currentMIPS->UpdateCore((CPUCore)g_Config.iCpuCore);
487 }
488 } else if (!strcmp(message, "window minimized")) {
489 if (!strcmp(value, "true")) {
490 gstate_c.skipDrawReason |= SKIPDRAW_WINDOW_MINIMIZED;
491 } else {
492 gstate_c.skipDrawReason &= ~SKIPDRAW_WINDOW_MINIMIZED;
493 }
494 } else if (!strcmp(message, "chat screen")) {
495 if (g_Config.bEnableNetworkChat) {
496 if (!chatButton_)
497 RecreateViews();
498
499 #if defined(USING_WIN_UI)
500 //temporary workaround for hotkey its freeze the ui when open chat screen using hotkey and native keyboard is enable
501 if (g_Config.bBypassOSKWithKeyboard) {
502 osm.Show("Disable windows native keyboard options to use ctrl + c hotkey", 2.0f);
503 } else {
504 UI::EventParams e{};
505 OnChatMenu.Trigger(e);
506 }
507 #else
508 UI::EventParams e{};
509 OnChatMenu.Trigger(e);
510 #endif
511 }
512 } else if (!strcmp(message, "app_resumed") && screenManager()->topScreen() == this) {
513 if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) {
514 if (!KeyMap::IsKeyMapped(DEVICE_ID_PAD_0, VIRTKEY_PAUSE) || !KeyMap::IsKeyMapped(DEVICE_ID_PAD_1, VIRTKEY_PAUSE)) {
515 // If it's a TV (so no built-in back button), and there's no back button mapped to a pad,
516 // use this as the fallback way to get into the menu.
517
518 screenManager()->push(new GamePauseScreen(gamePath_));
519 }
520 }
521 }
522 }
523
524 //tiltInputCurve implements a smooth deadzone as described here:
525 //http://www.gamasutra.com/blogs/JoshSutphin/20130416/190541/Doing_Thumbstick_Dead_Zones_Right.php
tiltInputCurve(float x)526 inline float tiltInputCurve(float x) {
527 const float deadzone = g_Config.fDeadzoneRadius;
528 const float factor = 1.0f / (1.0f - deadzone);
529
530 if (x > deadzone) {
531 return (x - deadzone) * (x - deadzone) * factor;
532 } else if (x < -deadzone) {
533 return -(x + deadzone) * (x + deadzone) * factor;
534 } else {
535 return 0.0f;
536 }
537 }
538
clamp1(float x)539 inline float clamp1(float x) {
540 if (x > 1.0f) return 1.0f;
541 if (x < -1.0f) return -1.0f;
542 return x;
543 }
544
touch(const TouchInput & touch)545 bool EmuScreen::touch(const TouchInput &touch) {
546 Core_NotifyActivity();
547
548 if (chatMenu_ && (touch.flags & TOUCH_DOWN) != 0 && !chatMenu_->Contains(touch.x, touch.y)) {
549 chatMenu_->Close();
550 if (chatButton_)
551 chatButton_->SetVisibility(UI::V_VISIBLE);
552 UI::EnableFocusMovement(false);
553 }
554
555 if (root_) {
556 root_->Touch(touch);
557 return true;
558 } else {
559 return false;
560 }
561 }
562
onVKeyDown(int virtualKeyCode)563 void EmuScreen::onVKeyDown(int virtualKeyCode) {
564 auto sc = GetI18NCategory("Screen");
565
566 switch (virtualKeyCode) {
567 case VIRTKEY_FASTFORWARD:
568 if (coreState == CORE_STEPPING) {
569 Core_EnableStepping(false);
570 }
571 PSP_CoreParameter().fastForward = true;
572 break;
573
574 case VIRTKEY_SPEED_TOGGLE:
575 // Cycle through enabled speeds.
576 if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL && g_Config.iFpsLimit1 >= 0) {
577 PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM1;
578 osm.Show(sc->T("fixed", "Speed: alternate"), 1.0);
579 } else if (PSP_CoreParameter().fpsLimit != FPSLimit::CUSTOM2 && g_Config.iFpsLimit2 >= 0) {
580 PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM2;
581 osm.Show(sc->T("SpeedCustom2", "Speed: alternate 2"), 1.0);
582 } else if (PSP_CoreParameter().fpsLimit != FPSLimit::NORMAL) {
583 PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
584 osm.Show(sc->T("standard", "Speed: standard"), 1.0);
585 }
586 break;
587
588 case VIRTKEY_SPEED_CUSTOM1:
589 if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL) {
590 PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM1;
591 osm.Show(sc->T("fixed", "Speed: alternate"), 1.0);
592 }
593 break;
594 case VIRTKEY_SPEED_CUSTOM2:
595 if (PSP_CoreParameter().fpsLimit == FPSLimit::NORMAL) {
596 PSP_CoreParameter().fpsLimit = FPSLimit::CUSTOM2;
597 osm.Show(sc->T("SpeedCustom2", "Speed: alternate 2"), 1.0);
598 }
599 break;
600
601 case VIRTKEY_PAUSE:
602 pauseTrigger_ = true;
603 break;
604
605 case VIRTKEY_FRAME_ADVANCE:
606 // If game is running, pause emulation immediately. Otherwise, advance a single frame.
607 if (Core_IsStepping())
608 {
609 frameStep_ = true;
610 Core_EnableStepping(false);
611 }
612 else if (!frameStep_)
613 {
614 Core_EnableStepping(true);
615 }
616 break;
617
618 case VIRTKEY_OPENCHAT:
619 if (g_Config.bEnableNetworkChat) {
620 UI::EventParams e{};
621 OnChatMenu.Trigger(e);
622 }
623 break;
624
625 case VIRTKEY_AXIS_SWAP:
626 KeyMap::SwapAxis();
627 break;
628
629 case VIRTKEY_DEVMENU:
630 {
631 UI::EventParams e{};
632 OnDevMenu.Trigger(e);
633 break;
634 }
635
636 #ifndef MOBILE_DEVICE
637 case VIRTKEY_RECORD:
638 {
639 if (g_Config.bDumpFrames == g_Config.bDumpAudio) {
640 g_Config.bDumpFrames = !g_Config.bDumpFrames;
641 g_Config.bDumpAudio = !g_Config.bDumpAudio;
642 } else {
643 // This hotkey should always toggle both audio and video together.
644 // So let's make sure that's the only outcome even if video OR audio was already being dumped.
645 if (g_Config.bDumpFrames) {
646 AVIDump::Stop();
647 AVIDump::Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
648 g_Config.bDumpAudio = true;
649 } else {
650 WAVDump::Reset();
651 g_Config.bDumpFrames = true;
652 }
653 }
654 break;
655 }
656 #endif
657
658 case VIRTKEY_REWIND:
659 if (SaveState::CanRewind()) {
660 SaveState::Rewind(&AfterSaveStateAction);
661 } else {
662 osm.Show(sc->T("norewind", "No rewind save states available"), 2.0);
663 }
664 break;
665 case VIRTKEY_SAVE_STATE:
666 SaveState::SaveSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction);
667 break;
668 case VIRTKEY_LOAD_STATE:
669 SaveState::LoadSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction);
670 break;
671 case VIRTKEY_NEXT_SLOT:
672 SaveState::NextSlot();
673 NativeMessageReceived("savestate_displayslot", "");
674 break;
675 case VIRTKEY_TOGGLE_FULLSCREEN:
676 System_SendMessage("toggle_fullscreen", "");
677 break;
678
679 case VIRTKEY_SCREENSHOT:
680 g_TakeScreenshot = true;
681 break;
682
683 case VIRTKEY_TEXTURE_DUMP:
684 g_Config.bSaveNewTextures = !g_Config.bSaveNewTextures;
685 if (g_Config.bSaveNewTextures) {
686 osm.Show(sc->T("saveNewTextures_true", "Textures will now be saved to your storage"), 2.0);
687 NativeMessageReceived("gpu_clearCache", "");
688 } else {
689 osm.Show(sc->T("saveNewTextures_false", "Texture saving was disabled"), 2.0);
690 }
691 break;
692 case VIRTKEY_TEXTURE_REPLACE:
693 g_Config.bReplaceTextures = !g_Config.bReplaceTextures;
694 if (g_Config.bReplaceTextures)
695 osm.Show(sc->T("replaceTextures_true", "Texture replacement enabled"), 2.0);
696 else
697 osm.Show(sc->T("replaceTextures_false", "Textures no longer are being replaced"), 2.0);
698 NativeMessageReceived("gpu_clearCache", "");
699 break;
700 case VIRTKEY_RAPID_FIRE:
701 __CtrlSetRapidFire(true);
702 break;
703 case VIRTKEY_MUTE_TOGGLE:
704 g_Config.bEnableSound = !g_Config.bEnableSound;
705 break;
706 }
707 }
708
onVKeyUp(int virtualKeyCode)709 void EmuScreen::onVKeyUp(int virtualKeyCode) {
710 auto sc = GetI18NCategory("Screen");
711
712 switch (virtualKeyCode) {
713 case VIRTKEY_FASTFORWARD:
714 PSP_CoreParameter().fastForward = false;
715 break;
716
717 case VIRTKEY_SPEED_CUSTOM1:
718 if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM1) {
719 PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
720 osm.Show(sc->T("standard", "Speed: standard"), 1.0);
721 }
722 break;
723 case VIRTKEY_SPEED_CUSTOM2:
724 if (PSP_CoreParameter().fpsLimit == FPSLimit::CUSTOM2) {
725 PSP_CoreParameter().fpsLimit = FPSLimit::NORMAL;
726 osm.Show(sc->T("standard", "Speed: standard"), 1.0);
727 }
728 break;
729
730 case VIRTKEY_RAPID_FIRE:
731 __CtrlSetRapidFire(false);
732 break;
733
734 default:
735 break;
736 }
737 }
738
key(const KeyInput & key)739 bool EmuScreen::key(const KeyInput &key) {
740 Core_NotifyActivity();
741
742 if (UI::IsFocusMovementEnabled()) {
743 if (UIScreen::key(key)) {
744 return true;
745 } else if ((key.flags & KEY_DOWN) != 0 && UI::IsEscapeKey(key)) {
746 if (chatMenu_)
747 chatMenu_->Close();
748 if (chatButton_)
749 chatButton_->SetVisibility(UI::V_VISIBLE);
750 UI::EnableFocusMovement(false);
751 return true;
752 }
753 }
754
755 return controlMapper_.Key(key, &pauseTrigger_);
756 }
757
axis(const AxisInput & axis)758 bool EmuScreen::axis(const AxisInput &axis) {
759 Core_NotifyActivity();
760
761 return controlMapper_.Axis(axis);
762 }
763
764 class GameInfoBGView : public UI::InertView {
765 public:
GameInfoBGView(const Path & gamePath,UI::LayoutParams * layoutParams)766 GameInfoBGView(const Path &gamePath, UI::LayoutParams *layoutParams) : InertView(layoutParams), gamePath_(gamePath) {
767 }
768
Draw(UIContext & dc)769 void Draw(UIContext &dc) override {
770 // Should only be called when visible.
771 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GAMEINFO_WANTBG);
772 dc.Flush();
773
774 // PIC1 is the loading image, so let's only draw if it's available.
775 if (ginfo && ginfo->pic1.texture) {
776 Draw::Texture *texture = ginfo->pic1.texture->GetTexture();
777 if (texture) {
778 dc.GetDrawContext()->BindTexture(0, texture);
779
780 double loadTime = ginfo->pic1.timeLoaded;
781 uint32_t color = alphaMul(color_, ease((time_now_d() - loadTime) * 3));
782 dc.Draw()->DrawTexRect(dc.GetBounds(), 0, 0, 1, 1, color);
783 dc.Flush();
784 dc.RebindTexture();
785 }
786 }
787 }
788
DescribeText() const789 std::string DescribeText() const override {
790 return "";
791 }
792
SetColor(uint32_t c)793 void SetColor(uint32_t c) {
794 color_ = c;
795 }
796
797 protected:
798 Path gamePath_;
799 uint32_t color_ = 0xFFC0C0C0;
800 };
801
CreateViews()802 void EmuScreen::CreateViews() {
803 using namespace UI;
804
805 auto dev = GetI18NCategory("Developer");
806 auto n = GetI18NCategory("Networking");
807 auto sc = GetI18NCategory("Screen");
808
809 const Bounds &bounds = screenManager()->getUIContext()->GetLayoutBounds();
810 InitPadLayout(bounds.w, bounds.h);
811
812 // Devices without a back button like iOS need an on-screen touch back button.
813 bool showPauseButton = !System_GetPropertyBool(SYSPROP_HAS_BACK_BUTTON) || g_Config.bShowTouchPause;
814
815 root_ = CreatePadLayout(bounds.w, bounds.h, &pauseTrigger_, showPauseButton, &controlMapper_);
816 if (g_Config.bShowDeveloperMenu) {
817 root_->Add(new Button(dev->T("DevMenu")))->OnClick.Handle(this, &EmuScreen::OnDevTools);
818 }
819
820 LinearLayout *buttons = new LinearLayout(Orientation::ORIENT_HORIZONTAL, new AnchorLayoutParams(bounds.centerX(), NONE, NONE, 60, true));
821 buttons->SetSpacing(20.0f);
822 root_->Add(buttons);
823
824 resumeButton_ = buttons->Add(new Button(dev->T("Resume")));
825 resumeButton_->OnClick.Handle(this, &EmuScreen::OnResume);
826 resumeButton_->SetVisibility(V_GONE);
827
828 resetButton_ = buttons->Add(new Button(dev->T("Reset")));
829 resetButton_->OnClick.Handle(this, &EmuScreen::OnReset);
830 resetButton_->SetVisibility(V_GONE);
831
832 cardboardDisableButton_ = root_->Add(new Button(sc->T("Cardboard VR OFF"), new AnchorLayoutParams(bounds.centerX(), NONE, NONE, 30, true)));
833 cardboardDisableButton_->OnClick.Handle(this, &EmuScreen::OnDisableCardboard);
834 cardboardDisableButton_->SetVisibility(V_GONE);
835 cardboardDisableButton_->SetScale(0.65f); // make it smaller - this button can be in the way otherwise.
836
837 if (g_Config.bEnableNetworkChat && g_Config.iChatButtonPosition != 8) {
838 AnchorLayoutParams *layoutParams = nullptr;
839 switch (g_Config.iChatButtonPosition) {
840 case 0:
841 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 80, NONE, NONE, 50, true);
842 break;
843 case 1:
844 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, bounds.centerX(), NONE, NONE, 50, true);
845 break;
846 case 2:
847 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, NONE, 80, 50, true);
848 break;
849 case 3:
850 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 80, 50, NONE, NONE, true);
851 break;
852 case 4:
853 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, bounds.centerX(), 50, NONE, NONE, true);
854 break;
855 case 5:
856 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, 50, 80, NONE, true);
857 break;
858 case 6:
859 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 80, bounds.centerY(), NONE, NONE, true);
860 break;
861 case 7:
862 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, NONE, bounds.centerY(), 80, NONE, true);
863 break;
864 default:
865 layoutParams = new AnchorLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 80, NONE, NONE, 50, true);
866 break;
867 }
868
869 ChoiceWithValueDisplay *btn = new ChoiceWithValueDisplay(&newChatMessages_, n->T("Chat"), layoutParams);
870 root_->Add(btn)->OnClick.Handle(this, &EmuScreen::OnChat);
871 chatButton_ = btn;
872 chatMenu_ = root_->Add(new ChatMenu(screenManager()->getUIContext()->GetBounds(), new LayoutParams(FILL_PARENT, FILL_PARENT)));
873 chatMenu_->SetVisibility(UI::V_GONE);
874 } else {
875 chatButton_ = nullptr;
876 chatMenu_ = nullptr;
877 }
878
879 saveStatePreview_ = new AsyncImageFileView(Path(), IS_FIXED, new AnchorLayoutParams(bounds.centerX(), 100, NONE, NONE, true));
880 saveStatePreview_->SetFixedSize(160, 90);
881 saveStatePreview_->SetColor(0x90FFFFFF);
882 saveStatePreview_->SetVisibility(V_GONE);
883 saveStatePreview_->SetCanBeFocused(false);
884 root_->Add(saveStatePreview_);
885 onScreenMessagesView_ = root_->Add(new OnScreenMessagesView(new AnchorLayoutParams((Size)bounds.w, (Size)bounds.h)));
886
887 GameInfoBGView *loadingBG = root_->Add(new GameInfoBGView(gamePath_, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)));
888 TextView *loadingTextView = root_->Add(new TextView(sc->T(PSP_GetLoading()), new AnchorLayoutParams(bounds.centerX(), NONE, NONE, 40, true)));
889 loadingTextView_ = loadingTextView;
890
891 static const ImageID symbols[4] = {
892 ImageID("I_CROSS"),
893 ImageID("I_CIRCLE"),
894 ImageID("I_SQUARE"),
895 ImageID("I_TRIANGLE"),
896 };
897
898 Spinner *loadingSpinner = root_->Add(new Spinner(symbols, ARRAY_SIZE(symbols), new AnchorLayoutParams(NONE, NONE, 45, 45, true)));
899 loadingSpinner_ = loadingSpinner;
900
901 loadingBG->SetTag("LoadingBG");
902 loadingTextView->SetTag("LoadingText");
903 loadingSpinner->SetTag("LoadingSpinner");
904
905 // Don't really need this, and it creates a lot of strings to translate...
906 loadingTextView->SetVisibility(V_GONE);
907 loadingTextView->SetShadow(true);
908
909 loadingViewColor_ = loadingSpinner->AddTween(new CallbackColorTween(0x00FFFFFF, 0x00FFFFFF, 0.2f, &bezierEaseInOut));
910 loadingViewColor_->SetCallback([loadingBG, loadingTextView, loadingSpinner](View *v, uint32_t c) {
911 loadingBG->SetColor(c & 0xFFC0C0C0);
912 loadingTextView->SetTextColor(c);
913 loadingSpinner->SetColor(alphaMul(c, 0.7f));
914 });
915 loadingViewColor_->Persist();
916
917 // We start invisible here, in case of recreated views.
918 loadingViewVisible_ = loadingSpinner->AddTween(new VisibilityTween(UI::V_INVISIBLE, UI::V_INVISIBLE, 0.2f, &bezierEaseInOut));
919 loadingViewVisible_->Persist();
920 loadingViewVisible_->Finish.Add([loadingBG, loadingSpinner](EventParams &p) {
921 loadingBG->SetVisibility(p.v->GetVisibility());
922
923 // If we just became invisible, flush BGs since we don't need them anymore.
924 // Saves some VRAM for the game, but don't do it before we fade out...
925 if (p.v->GetVisibility() == V_INVISIBLE) {
926 g_gameInfoCache->FlushBGs();
927 // And we can go away too. This means the tween will never run again.
928 loadingBG->SetVisibility(V_GONE);
929 loadingSpinner->SetVisibility(V_GONE);
930 }
931 return EVENT_DONE;
932 });
933 }
934
OnDevTools(UI::EventParams & params)935 UI::EventReturn EmuScreen::OnDevTools(UI::EventParams ¶ms) {
936 auto dev = GetI18NCategory("Developer");
937 DevMenu *devMenu = new DevMenu(dev);
938 if (params.v)
939 devMenu->SetPopupOrigin(params.v);
940 screenManager()->push(devMenu);
941 return UI::EVENT_DONE;
942 }
943
OnDisableCardboard(UI::EventParams & params)944 UI::EventReturn EmuScreen::OnDisableCardboard(UI::EventParams ¶ms) {
945 g_Config.bEnableCardboardVR = false;
946 return UI::EVENT_DONE;
947 }
948
OnChat(UI::EventParams & params)949 UI::EventReturn EmuScreen::OnChat(UI::EventParams ¶ms) {
950 if (chatButton_ != nullptr && chatButton_->GetVisibility() == UI::V_VISIBLE) {
951 chatButton_->SetVisibility(UI::V_GONE);
952 }
953 if (chatMenu_ != nullptr) {
954 chatMenu_->SetVisibility(UI::V_VISIBLE);
955
956 #if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(SDL)
957 UI::EnableFocusMovement(true);
958 root_->SetDefaultFocusView(chatMenu_);
959
960 chatMenu_->SetFocus();
961 UI::View *focused = UI::GetFocusedView();
962 if (focused) {
963 root_->SubviewFocused(focused);
964 }
965 #endif
966 }
967 return UI::EVENT_DONE;
968 }
969
OnResume(UI::EventParams & params)970 UI::EventReturn EmuScreen::OnResume(UI::EventParams ¶ms) {
971 if (coreState == CoreState::CORE_RUNTIME_ERROR) {
972 // Force it!
973 Memory::MemFault_IgnoreLastCrash();
974 coreState = CoreState::CORE_RUNNING;
975 }
976 return UI::EVENT_DONE;
977 }
978
OnReset(UI::EventParams & params)979 UI::EventReturn EmuScreen::OnReset(UI::EventParams ¶ms) {
980 if (coreState == CoreState::CORE_RUNTIME_ERROR) {
981 NativeMessageReceived("reset", "");
982 }
983 return UI::EVENT_DONE;
984 }
985
update()986 void EmuScreen::update() {
987 using namespace UI;
988
989 UIScreen::update();
990 onScreenMessagesView_->SetVisibility(g_Config.bShowOnScreenMessages ? V_VISIBLE : V_GONE);
991 resumeButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR && Memory::MemFault_MayBeResumable() ? V_VISIBLE : V_GONE);
992 resetButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR ? V_VISIBLE : V_GONE);
993
994 if (chatButton_ && chatMenu_) {
995 if (chatMenu_->GetVisibility() != V_GONE) {
996 chatMessages_ = GetChatMessageCount();
997 newChatMessages_ = 0;
998 } else {
999 int diff = GetChatMessageCount() - chatMessages_;
1000 // Cap the count at 50.
1001 newChatMessages_ = diff > 50 ? 50 : diff;
1002 }
1003 }
1004
1005 if (bootPending_) {
1006 bootGame(gamePath_);
1007 }
1008
1009 // Simply forcibly update to the current screen size every frame. Doesn't cost much.
1010 // If bounds is set to be smaller than the actual pixel resolution of the display, respect that.
1011 // TODO: Should be able to use g_dpi_scale here instead. Might want to store the dpi scale in the UI context too.
1012
1013 #ifndef _WIN32
1014 const Bounds &bounds = screenManager()->getUIContext()->GetBounds();
1015 PSP_CoreParameter().pixelWidth = pixel_xres * bounds.w / dp_xres;
1016 PSP_CoreParameter().pixelHeight = pixel_yres * bounds.h / dp_yres;
1017 #endif
1018
1019 if (!invalid_) {
1020 UpdateUIState(coreState != CORE_RUNTIME_ERROR ? UISTATE_INGAME : UISTATE_EXCEPTION);
1021 }
1022
1023 if (errorMessage_.size()) {
1024 auto err = GetI18NCategory("Error");
1025 std::string errLoadingFile = gamePath_.ToVisualString() + "\n";
1026 errLoadingFile.append(err->T("Error loading file", "Could not load game"));
1027 errLoadingFile.append(" ");
1028 errLoadingFile.append(err->T(errorMessage_.c_str()));
1029
1030 screenManager()->push(new PromptScreen(errLoadingFile, "OK", ""));
1031 errorMessage_ = "";
1032 quit_ = true;
1033 return;
1034 }
1035
1036 if (invalid_)
1037 return;
1038
1039 controlMapper_.Update();
1040
1041 if (pauseTrigger_) {
1042 pauseTrigger_ = false;
1043 screenManager()->push(new GamePauseScreen(gamePath_));
1044 }
1045
1046 if (saveStatePreview_ && !bootPending_) {
1047 int currentSlot = SaveState::GetCurrentSlot();
1048 if (saveStateSlot_ != currentSlot) {
1049 saveStateSlot_ = currentSlot;
1050
1051 Path fn;
1052 if (SaveState::HasSaveInSlot(gamePath_, currentSlot)) {
1053 fn = SaveState::GenerateSaveSlotFilename(gamePath_, currentSlot, SaveState::SCREENSHOT_EXTENSION);
1054 }
1055
1056 saveStatePreview_->SetFilename(fn);
1057 if (!fn.empty()) {
1058 saveStatePreview_->SetVisibility(UI::V_VISIBLE);
1059 saveStatePreviewShownTime_ = time_now_d();
1060 } else {
1061 saveStatePreview_->SetVisibility(UI::V_GONE);
1062 }
1063 }
1064
1065 if (saveStatePreview_->GetVisibility() == UI::V_VISIBLE) {
1066 double endTime = saveStatePreviewShownTime_ + 2.0;
1067 float alpha = clamp_value((endTime - time_now_d()) * 4.0, 0.0, 1.0);
1068 saveStatePreview_->SetColor(colorAlpha(0x00FFFFFF, alpha));
1069
1070 if (time_now_d() - saveStatePreviewShownTime_ > 2) {
1071 saveStatePreview_->SetVisibility(UI::V_GONE);
1072 }
1073 }
1074 }
1075
1076 }
1077
checkPowerDown()1078 void EmuScreen::checkPowerDown() {
1079 if (coreState == CORE_POWERDOWN && !PSP_IsIniting()) {
1080 if (PSP_IsInited()) {
1081 PSP_Shutdown();
1082 }
1083 INFO_LOG(SYSTEM, "SELF-POWERDOWN!");
1084 screenManager()->switchScreen(new MainScreen());
1085 bootPending_ = false;
1086 invalid_ = true;
1087 }
1088 }
1089
DrawDebugStats(DrawBuffer * draw2d,const Bounds & bounds)1090 static void DrawDebugStats(DrawBuffer *draw2d, const Bounds &bounds) {
1091 FontID ubuntu24("UBUNTU24");
1092
1093 float left = std::max(bounds.w / 2 - 20.0f, 550.0f);
1094 float right = bounds.w - left - 20.0f;
1095
1096 char statbuf[4096];
1097 __DisplayGetDebugStats(statbuf, sizeof(statbuf));
1098 draw2d->SetFontScale(.7f, .7f);
1099 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + 11, bounds.y + 31, left, bounds.h - 30, 0xc0000000, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1100 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + 10, bounds.y + 30, left, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1101
1102 __SasGetDebugStats(statbuf, sizeof(statbuf));
1103 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + left + 21, bounds.y + 31, right, bounds.h - 30, 0xc0000000, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1104 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + left + 20, bounds.y + 30, right, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1105 draw2d->SetFontScale(1.0f, 1.0f);
1106 }
1107
CPUCoreAsString(int core)1108 static const char *CPUCoreAsString(int core) {
1109 switch (core) {
1110 case 0: return "Interpreter";
1111 case 1: return "JIT";
1112 case 2: return "IR Interpreter";
1113 default: return "N/A";
1114 }
1115 }
1116
DrawCrashDump(UIContext * ctx)1117 static void DrawCrashDump(UIContext *ctx) {
1118 const ExceptionInfo &info = Core_GetExceptionInfo();
1119
1120 FontID ubuntu24("UBUNTU24");
1121 char statbuf[4096];
1122 char versionString[256];
1123 snprintf(versionString, sizeof(versionString), "%s", PPSSPP_GIT_VERSION);
1124
1125 // TODO: Draw a lot more information. Full register set, and so on.
1126
1127 #ifdef _DEBUG
1128 char build[] = "debug";
1129 #else
1130 char build[] = "release";
1131 #endif
1132
1133 std::string sysName = System_GetProperty(SYSPROP_NAME);
1134 int sysVersion = System_GetPropertyInt(SYSPROP_SYSTEMVERSION);
1135
1136 // First column
1137 ctx->Flush();
1138 int x = 20 + System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_LEFT);
1139 int y = 50 + System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_TOP);
1140
1141 int columnWidth = (ctx->GetBounds().w - x - 10) / 2;
1142 int height = ctx->GetBounds().h;
1143
1144 ctx->PushScissor(Bounds(x, y, columnWidth, height));
1145
1146 snprintf(statbuf, sizeof(statbuf), R"(%s
1147 %s (%s)
1148 %s (%s)
1149 %s v%d (%s)
1150 )",
1151 ExceptionTypeAsString(info.type),
1152 g_paramSFO.GetDiscID().c_str(), g_paramSFO.GetValueString("TITLE").c_str(),
1153 versionString, build,
1154 sysName.c_str(), sysVersion, GetCompilerABI()
1155 );
1156
1157 ctx->Draw()->SetFontScale(.7f, .7f);
1158 ctx->Draw()->DrawTextShadow(ubuntu24, statbuf, x, y, 0xFFFFFFFF);
1159 y += 140;
1160
1161 if (info.type == ExceptionType::MEMORY) {
1162 snprintf(statbuf, sizeof(statbuf), R"(
1163 Access: %s at %08x
1164 PC: %08x
1165 %s)",
1166 MemoryExceptionTypeAsString(info.memory_type),
1167 info.address,
1168 info.pc,
1169 info.info.c_str());
1170 ctx->Draw()->DrawTextShadow(ubuntu24, statbuf, x, y, 0xFFFFFFFF);
1171 y += 180;
1172 } else if (info.type == ExceptionType::BAD_EXEC_ADDR) {
1173 snprintf(statbuf, sizeof(statbuf), R"(
1174 Destination: %s to %08x
1175 PC: %08x)",
1176 ExecExceptionTypeAsString(info.exec_type),
1177 info.address,
1178 info.pc);
1179 ctx->Draw()->DrawTextShadow(ubuntu24, statbuf, x, y, 0xFFFFFFFF);
1180 y += 180;
1181 } else {
1182 snprintf(statbuf, sizeof(statbuf), R"(
1183 BREAK
1184 )");
1185 ctx->Draw()->DrawTextShadow(ubuntu24, statbuf, x, y, 0xFFFFFFFF);
1186 y += 180;
1187 }
1188
1189 std::string kernelState = __KernelStateSummary();
1190
1191 ctx->Draw()->DrawTextShadow(ubuntu24, kernelState.c_str(), x, y, 0xFFFFFFFF);
1192
1193 ctx->PopScissor();
1194
1195 // Draw some additional stuff to the right.
1196
1197 x += columnWidth + 10;
1198 y = 50;
1199 snprintf(statbuf, sizeof(statbuf),
1200 "CPU Core: %s (flags: %08x)\n"
1201 "Locked CPU freq: %d MHz\n"
1202 "Cheats: %s, Plugins: %s\n",
1203 CPUCoreAsString(g_Config.iCpuCore), g_Config.uJitDisableFlags,
1204 g_Config.iLockedCPUSpeed,
1205 CheatsInEffect() ? "Y" : "N", HLEPlugins::HasEnabled() ? "Y" : "N");
1206
1207 ctx->Draw()->DrawTextShadow(ubuntu24, statbuf, x, y, 0xFFFFFFFF);
1208 }
1209
DrawAudioDebugStats(DrawBuffer * draw2d,const Bounds & bounds)1210 static void DrawAudioDebugStats(DrawBuffer *draw2d, const Bounds &bounds) {
1211 FontID ubuntu24("UBUNTU24");
1212 char statbuf[4096] = { 0 };
1213 __AudioGetDebugStats(statbuf, sizeof(statbuf));
1214 draw2d->SetFontScale(0.7f, 0.7f);
1215 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + 11, bounds.y + 31, bounds.w - 20, bounds.h - 30, 0xc0000000, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1216 draw2d->DrawTextRect(ubuntu24, statbuf, bounds.x + 10, bounds.y + 30, bounds.w - 20, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII | FLAG_WRAP_TEXT);
1217 draw2d->SetFontScale(1.0f, 1.0f);
1218 }
1219
DrawFPS(DrawBuffer * draw2d,const Bounds & bounds)1220 static void DrawFPS(DrawBuffer *draw2d, const Bounds &bounds) {
1221 FontID ubuntu24("UBUNTU24");
1222 float vps, fps, actual_fps;
1223 __DisplayGetFPS(&vps, &fps, &actual_fps);
1224 char fpsbuf[256];
1225 switch (g_Config.iShowFPSCounter) {
1226 case 1:
1227 snprintf(fpsbuf, sizeof(fpsbuf), "Speed: %0.1f%%", vps / (59.94f / 100.0f)); break;
1228 case 2:
1229 snprintf(fpsbuf, sizeof(fpsbuf), "FPS: %0.1f", actual_fps); break;
1230 case 3:
1231 snprintf(fpsbuf, sizeof(fpsbuf), "%0.0f/%0.0f (%0.1f%%)", actual_fps, fps, vps / (59.94f / 100.0f)); break;
1232 default:
1233 return;
1234 }
1235
1236 draw2d->SetFontScale(0.7f, 0.7f);
1237 draw2d->DrawText(ubuntu24, fpsbuf, bounds.x2() - 8, 12, 0xc0000000, ALIGN_TOPRIGHT | FLAG_DYNAMIC_ASCII);
1238 draw2d->DrawText(ubuntu24, fpsbuf, bounds.x2() - 10, 10, 0xFF3fFF3f, ALIGN_TOPRIGHT | FLAG_DYNAMIC_ASCII);
1239 draw2d->SetFontScale(1.0f, 1.0f);
1240 }
1241
DrawFrameTimes(UIContext * ctx,const Bounds & bounds)1242 static void DrawFrameTimes(UIContext *ctx, const Bounds &bounds) {
1243 FontID ubuntu24("UBUNTU24");
1244 int valid, pos;
1245 double *sleepHistory;
1246 double *history = __DisplayGetFrameTimes(&valid, &pos, &sleepHistory);
1247 int scale = 7000;
1248 int width = 600;
1249
1250 ctx->Flush();
1251 ctx->BeginNoTex();
1252 int bottom = bounds.y2();
1253 for (int i = 0; i < valid; ++i) {
1254 double activeTime = history[i] - sleepHistory[i];
1255 ctx->Draw()->vLine(bounds.x + i, bottom, bottom - activeTime * scale, 0xFF3FFF3F);
1256 ctx->Draw()->vLine(bounds.x + i, bottom - activeTime * scale, bottom - history[i] * scale, 0x7F3FFF3F);
1257 }
1258 ctx->Draw()->vLine(bounds.x + pos, bottom, bottom - 512, 0xFFff3F3f);
1259
1260 ctx->Draw()->hLine(bounds.x, bottom - 0.0333 * scale, bounds.x + width, 0xFF3f3Fff);
1261 ctx->Draw()->hLine(bounds.x, bottom - 0.0167 * scale, bounds.x + width, 0xFF3f3Fff);
1262
1263 ctx->Flush();
1264 ctx->Begin();
1265 ctx->Draw()->SetFontScale(0.5f, 0.5f);
1266 ctx->Draw()->DrawText(ubuntu24, "33.3ms", bounds.x + width, bottom - 0.0333 * scale, 0xFF3f3Fff, ALIGN_BOTTOMLEFT | FLAG_DYNAMIC_ASCII);
1267 ctx->Draw()->DrawText(ubuntu24, "16.7ms", bounds.x + width, bottom - 0.0167 * scale, 0xFF3f3Fff, ALIGN_BOTTOMLEFT | FLAG_DYNAMIC_ASCII);
1268 ctx->Draw()->SetFontScale(1.0f, 1.0f);
1269 }
1270
preRender()1271 void EmuScreen::preRender() {
1272 using namespace Draw;
1273 DrawContext *draw = screenManager()->getDrawContext();
1274 draw->BeginFrame();
1275 // Here we do NOT bind the backbuffer or clear the screen, unless non-buffered.
1276 // The emuscreen is different than the others - we really want to allow the game to render to framebuffers
1277 // before we ever bind the backbuffer for rendering. On mobile GPUs, switching back and forth between render
1278 // targets is a mortal sin so it's very important that we don't bind the backbuffer unnecessarily here.
1279 // We only bind it in FramebufferManager::CopyDisplayToOutput (unless non-buffered)...
1280 // We do, however, start the frame in other ways.
1281
1282 bool useBufferedRendering = g_Config.iRenderingMode != FB_NON_BUFFERED_MODE;
1283 if ((!useBufferedRendering && !g_Config.bSoftwareRendering) || Core_IsStepping()) {
1284 // We need to clear here already so that drawing during the frame is done on a clean slate.
1285 if (Core_IsStepping() && gpuStats.numFlips != 0) {
1286 draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::KEEP, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_BackBuffer");
1287 } else {
1288 draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR, 0xFF000000 }, "EmuScreen_BackBuffer");
1289 }
1290
1291 Viewport viewport;
1292 viewport.TopLeftX = 0;
1293 viewport.TopLeftY = 0;
1294 viewport.Width = pixel_xres;
1295 viewport.Height = pixel_yres;
1296 viewport.MaxDepth = 1.0;
1297 viewport.MinDepth = 0.0;
1298 draw->SetViewports(1, &viewport);
1299 }
1300 draw->SetTargetSize(pixel_xres, pixel_yres);
1301 }
1302
postRender()1303 void EmuScreen::postRender() {
1304 Draw::DrawContext *draw = screenManager()->getDrawContext();
1305 if (!draw)
1306 return;
1307 if (stopRender_)
1308 draw->WipeQueue();
1309 draw->EndFrame();
1310 }
1311
render()1312 void EmuScreen::render() {
1313 using namespace Draw;
1314
1315 DrawContext *thin3d = screenManager()->getDrawContext();
1316 if (!thin3d)
1317 return; // shouldn't really happen but I've seen a suspicious stack trace..
1318
1319 if (invalid_) {
1320 // Loading, or after shutdown?
1321 if (loadingTextView_->GetVisibility() == UI::V_VISIBLE)
1322 loadingTextView_->SetText(PSP_GetLoading());
1323
1324 // It's possible this might be set outside PSP_RunLoopFor().
1325 // In this case, we need to double check it here.
1326 checkPowerDown();
1327 thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_Invalid");
1328 renderUI();
1329 return;
1330 }
1331
1332 // Freeze-frame functionality (loads a savestate on every frame).
1333 if (PSP_CoreParameter().freezeNext) {
1334 PSP_CoreParameter().frozen = true;
1335 PSP_CoreParameter().freezeNext = false;
1336 SaveState::SaveToRam(freezeState_);
1337 } else if (PSP_CoreParameter().frozen) {
1338 std::string errorString;
1339 if (CChunkFileReader::ERROR_NONE != SaveState::LoadFromRam(freezeState_, &errorString)) {
1340 ERROR_LOG(SAVESTATE, "Failed to load freeze state (%s). Unfreezing.", errorString.c_str());
1341 PSP_CoreParameter().frozen = false;
1342 }
1343 }
1344
1345 Core_UpdateDebugStats(g_Config.bShowDebugStats || g_Config.bLogFrameDrops);
1346
1347 PSP_BeginHostFrame();
1348
1349 PSP_RunLoopWhileState();
1350
1351 // Hopefully coreState is now CORE_NEXTFRAME
1352 switch (coreState) {
1353 case CORE_NEXTFRAME:
1354 // Reached the end of the frame, all good. Set back to running for the next frame
1355 coreState = CORE_RUNNING;
1356 break;
1357 case CORE_STEPPING:
1358 case CORE_RUNTIME_ERROR:
1359 {
1360 // If there's an exception, display information.
1361 const ExceptionInfo &info = Core_GetExceptionInfo();
1362 if (info.type != ExceptionType::NONE) {
1363 // Clear to blue background screen
1364 bool dangerousSettings = !Reporting::IsSupported();
1365 uint32_t color = dangerousSettings ? 0xFF900050 : 0xFF900000;
1366 thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE, color }, "EmuScreen_RuntimeError");
1367 // The info is drawn later in renderUI
1368 } else {
1369 // If we're stepping, it's convenient not to clear the screen entirely, so we copy display to output.
1370 // This won't work in non-buffered, but that's fine.
1371 thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_Stepping");
1372 // Just to make sure.
1373 if (PSP_IsInited()) {
1374 gpu->CopyDisplayToOutput(true);
1375 }
1376 }
1377 break;
1378 }
1379 default:
1380 // Didn't actually reach the end of the frame, ran out of the blockTicks cycles.
1381 // In this case we need to bind and wipe the backbuffer, at least.
1382 // It's possible we never ended up outputted anything - make sure we have the backbuffer cleared
1383 thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR }, "EmuScreen_NoFrame");
1384 break;
1385 }
1386
1387 checkPowerDown();
1388
1389 PSP_EndHostFrame();
1390 if (invalid_)
1391 return;
1392
1393 if (hasVisibleUI()) {
1394 // In most cases, this should already be bound and a no-op.
1395 thin3d->BindFramebufferAsRenderTarget(nullptr, { RPAction::KEEP, RPAction::DONT_CARE, RPAction::DONT_CARE }, "EmuScreen_UI");
1396 cardboardDisableButton_->SetVisibility(g_Config.bEnableCardboardVR ? UI::V_VISIBLE : UI::V_GONE);
1397 screenManager()->getUIContext()->BeginFrame();
1398 renderUI();
1399 }
1400 }
1401
hasVisibleUI()1402 bool EmuScreen::hasVisibleUI() {
1403 // Regular but uncommon UI.
1404 if (saveStatePreview_->GetVisibility() != UI::V_GONE || loadingSpinner_->GetVisibility() == UI::V_VISIBLE)
1405 return true;
1406 if (!osm.IsEmpty() || g_Config.bShowTouchControls || g_Config.iShowFPSCounter != 0)
1407 return true;
1408 if (g_Config.bEnableCardboardVR || g_Config.bEnableNetworkChat)
1409 return true;
1410 // Debug UI.
1411 if (g_Config.bShowDebugStats || g_Config.bShowDeveloperMenu || g_Config.bShowAudioDebug || g_Config.bShowFrameProfiler)
1412 return true;
1413
1414 // Exception information.
1415 if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_STEPPING) {
1416 return true;
1417 }
1418
1419 return false;
1420 }
1421
renderUI()1422 void EmuScreen::renderUI() {
1423 using namespace Draw;
1424
1425 DrawContext *thin3d = screenManager()->getDrawContext();
1426 UIContext *ctx = screenManager()->getUIContext();
1427 ctx->BeginFrame();
1428 // This sets up some important states but not the viewport.
1429 ctx->Begin();
1430
1431 Viewport viewport;
1432 viewport.TopLeftX = 0;
1433 viewport.TopLeftY = 0;
1434 viewport.Width = pixel_xres;
1435 viewport.Height = pixel_yres;
1436 viewport.MaxDepth = 1.0;
1437 viewport.MinDepth = 0.0;
1438 thin3d->SetViewports(1, &viewport);
1439
1440 if (root_) {
1441 UI::LayoutViewHierarchy(*ctx, root_, false);
1442 root_->Draw(*ctx);
1443 }
1444
1445 DrawBuffer *draw2d = ctx->Draw();
1446
1447 if (g_Config.bShowDebugStats && !invalid_) {
1448 DrawDebugStats(draw2d, ctx->GetLayoutBounds());
1449 }
1450
1451 if (g_Config.bShowAudioDebug && !invalid_) {
1452 DrawAudioDebugStats(draw2d, ctx->GetLayoutBounds());
1453 }
1454
1455 if (g_Config.iShowFPSCounter && !invalid_) {
1456 DrawFPS(draw2d, ctx->GetLayoutBounds());
1457 }
1458
1459 if (g_Config.bDrawFrameGraph && !invalid_) {
1460 DrawFrameTimes(ctx, ctx->GetLayoutBounds());
1461 }
1462
1463 #if !PPSSPP_PLATFORM(UWP)
1464 if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN && g_Config.bShowAllocatorDebug) {
1465 DrawAllocatorVis(ctx, gpu);
1466 }
1467
1468 if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN && g_Config.bShowGpuProfile) {
1469 DrawGPUProfilerVis(ctx, gpu);
1470 }
1471
1472 #endif
1473
1474 #ifdef USE_PROFILER
1475 if (g_Config.bShowFrameProfiler && !invalid_) {
1476 DrawProfile(*ctx);
1477 }
1478 #endif
1479
1480 if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_STEPPING) {
1481 const ExceptionInfo &info = Core_GetExceptionInfo();
1482 if (info.type != ExceptionType::NONE) {
1483 DrawCrashDump(ctx);
1484 }
1485 }
1486
1487 ctx->Flush();
1488 }
1489
autoLoad()1490 void EmuScreen::autoLoad() {
1491 int autoSlot = -1;
1492
1493 //check if save state has save, if so, load
1494 switch (g_Config.iAutoLoadSaveState) {
1495 case (int)AutoLoadSaveState::OFF: // "AutoLoad Off"
1496 return;
1497 case (int)AutoLoadSaveState::OLDEST: // "Oldest Save"
1498 autoSlot = SaveState::GetOldestSlot(gamePath_);
1499 break;
1500 case (int)AutoLoadSaveState::NEWEST: // "Newest Save"
1501 autoSlot = SaveState::GetNewestSlot(gamePath_);
1502 break;
1503 default: // try the specific save state slot specified
1504 autoSlot = (SaveState::HasSaveInSlot(gamePath_, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1;
1505 break;
1506 }
1507
1508 if (g_Config.iAutoLoadSaveState && autoSlot != -1) {
1509 SaveState::LoadSlot(gamePath_, autoSlot, &AfterSaveStateAction);
1510 g_Config.iCurrentStateSlot = autoSlot;
1511 }
1512 }
1513
resized()1514 void EmuScreen::resized() {
1515 RecreateViews();
1516 }
1517