CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/headless/Headless.cpp
Views: 1401
// Headless version of PPSSPP, for testing using http://code.google.com/p/pspautotests/ .1// See headless.txt.2// To build on non-windows systems, just run CMake in the SDL directory, it will build both a normal ppsspp and the headless version.3// Example command line to run a test in the VS debugger (useful to debug failures):4// > --root pspautotests/tests/../ --compare --timeout=5 --graphics=software pspautotests/tests/cpu/cpu_alu/cpu_alu.prx56#include "ppsspp_config.h"7#include <cstdio>8#include <cstdlib>9#include <limits>10#if PPSSPP_PLATFORM(ANDROID)11#include <jni.h>12#endif1314#include "Common/Profiler/Profiler.h"15#include "Common/System/NativeApp.h"16#include "Common/System/Request.h"17#include "Common/System/System.h"1819#include "Common/CommonWindows.h"20#if PPSSPP_PLATFORM(WINDOWS)21#include <timeapi.h>22#else23#include <csignal>24#endif25#include "Common/CPUDetect.h"26#include "Common/File/VFS/VFS.h"27#include "Common/File/VFS/ZipFileReader.h"28#include "Common/File/VFS/DirectoryReader.h"29#include "Common/File/FileUtil.h"30#include "Common/GraphicsContext.h"31#include "Common/TimeUtil.h"32#include "Common/Thread/ThreadManager.h"33#include "Core/Config.h"34#include "Core/ConfigValues.h"35#include "Core/Core.h"36#include "Core/CoreTiming.h"37#include "Core/System.h"38#include "Core/WebServer.h"39#include "Core/HLE/sceUtility.h"40#include "Core/SaveState.h"41#include "GPU/Common/FramebufferManagerCommon.h"42#include "Common/Log.h"43#include "Common/Log/LogManager.h"4445#include "Compare.h"46#include "HeadlessHost.h"47#if defined(_WIN32)48#include "WindowsHeadlessHost.h"49#elif defined(SDL)50#include "SDLHeadlessHost.h"51#endif5253static HeadlessHost *g_headlessHost;5455#if PPSSPP_PLATFORM(ANDROID)56JNIEnv *getEnv() {57return nullptr;58}5960jclass findClass(const char *name) {61return nullptr;62}6364bool System_AudioRecordingIsAvailable() { return false; }65bool System_AudioRecordingState() { return false; }66#endif6768class PrintfLogger : public LogListener {69public:70void Log(const LogMessage &message) override {71switch (message.level) {72case LogLevel::LVERBOSE:73fprintf(stderr, "V %s", message.msg.c_str());74break;75case LogLevel::LDEBUG:76fprintf(stderr, "D %s", message.msg.c_str());77break;78case LogLevel::LINFO:79fprintf(stderr, "I %s", message.msg.c_str());80break;81case LogLevel::LERROR:82fprintf(stderr, "E %s", message.msg.c_str());83break;84case LogLevel::LWARNING:85fprintf(stderr, "W %s", message.msg.c_str());86break;87case LogLevel::LNOTICE:88default:89fprintf(stderr, "N %s", message.msg.c_str());90break;91}92}93};9495// Temporary hacks around annoying linking errors.96void NativeFrame(GraphicsContext *graphicsContext) { }97void NativeResized() { }9899std::string System_GetProperty(SystemProperty prop) { return ""; }100std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }101int64_t System_GetPropertyInt(SystemProperty prop) {102if (prop == SYSPROP_SYSTEMVERSION)103return 31;104return -1;105}106float System_GetPropertyFloat(SystemProperty prop) { return -1.0f; }107bool System_GetPropertyBool(SystemProperty prop) {108switch (prop) {109case SYSPROP_CAN_JIT:110return true;111case SYSPROP_SKIP_UI:112return true;113default:114return false;115}116}117void System_Notify(SystemNotification notification) {}118void System_PostUIMessage(UIMessage message, const std::string ¶m) {}119bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) {120switch (type) {121case SystemRequestType::SEND_DEBUG_OUTPUT:122if (g_headlessHost) {123g_headlessHost->SendDebugOutput(param1);124return true;125}126return false;127case SystemRequestType::SEND_DEBUG_SCREENSHOT:128if (g_headlessHost) {129g_headlessHost->SendDebugScreenshot((const u8 *)param1.data(), (uint32_t)(param1.size() / param3), param3);130return true;131}132return false;133default:134return false;135}136}137void System_AskForPermission(SystemPermission permission) {}138PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; }139void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; }140void System_AudioClear() {}141void System_AudioPushSamples(const s32 *audio, int numSamples) {}142143// TODO: To avoid having to define these here, these should probably be turned into system "requests".144bool NativeSaveSecret(std::string_view nameOfSecret, std::string_view data) { return false; }145std::string NativeLoadSecret(std::string_view nameOfSecret) {146return "";147}148149int printUsage(const char *progname, const char *reason)150{151if (reason != NULL)152fprintf(stderr, "Error: %s\n\n", reason);153fprintf(stderr, "PPSSPP Headless\n");154fprintf(stderr, "This is primarily meant as a non-interactive test tool.\n\n");155fprintf(stderr, "Usage: %s file.elf... [options]\n\n", progname);156fprintf(stderr, "Options:\n");157fprintf(stderr, " -m, --mount umd.cso mount iso on umd1:\n");158fprintf(stderr, " -r, --root some/path mount path on host0: (elfs must be in here)\n");159fprintf(stderr, " -l, --log full log output, not just emulated printfs\n");160fprintf(stderr, " --debugger=PORT enable websocket debugger and break at start\n");161162fprintf(stderr, " --graphics=BACKEND use a different gpu backend\n");163fprintf(stderr, " options: gles, software, directx9, etc.\n");164fprintf(stderr, " --screenshot=FILE compare against a screenshot\n");165fprintf(stderr, " --max-mse=NUMBER maximum allowed MSE error for screenshot\n");166fprintf(stderr, " --timeout=SECONDS abort test it if takes longer than SECONDS\n");167168fprintf(stderr, " -v, --verbose show the full passed/failed result\n");169fprintf(stderr, " -i use the interpreter\n");170fprintf(stderr, " --ir use ir interpreter\n");171fprintf(stderr, " -j use jit (default)\n");172fprintf(stderr, " -c, --compare compare with output in file.expected\n");173fprintf(stderr, " --bench run multiple times and output speed\n");174fprintf(stderr, "\nSee headless.txt for details.\n");175176return 1;177}178179static HeadlessHost *getHost(GPUCore gpuCore) {180switch (gpuCore) {181case GPUCORE_SOFTWARE:182return new HeadlessHost();183#ifdef HEADLESSHOST_CLASS184default:185return new HEADLESSHOST_CLASS();186#else187default:188return new HeadlessHost();189#endif190}191}192193struct AutoTestOptions {194double timeout;195double maxScreenshotError;196bool compare : 1;197bool verbose : 1;198bool bench : 1;199};200201bool RunAutoTest(HeadlessHost *headlessHost, CoreParameter &coreParameter, const AutoTestOptions &opt) {202// Kinda ugly, trying to guesstimate the test name from filename...203currentTestName = GetTestName(coreParameter.fileToStart);204205std::string output;206if (opt.compare || opt.bench)207coreParameter.collectDebugOutput = &output;208209std::string error_string;210if (!PSP_InitStart(coreParameter, &error_string)) {211fprintf(stderr, "Failed to start '%s'. Error: %s\n", coreParameter.fileToStart.c_str(), error_string.c_str());212printf("TESTERROR\n");213TeamCityPrint("testIgnored name='%s' message='PRX/ELF missing'", currentTestName.c_str());214GitHubActionsPrint("error", "PRX/ELF missing for %s", currentTestName.c_str());215return false;216}217218TeamCityPrint("testStarted name='%s' captureStandardOutput='true'", currentTestName.c_str());219220if (opt.compare)221headlessHost->SetComparisonScreenshot(ExpectedScreenshotFromFilename(coreParameter.fileToStart), opt.maxScreenshotError);222223while (!PSP_InitUpdate(&error_string))224sleep_ms(1);225if (!PSP_IsInited()) {226TeamCityPrint("testFailed name='%s' message='Startup failed'", currentTestName.c_str());227TeamCityPrint("testFinished name='%s'", currentTestName.c_str());228GitHubActionsPrint("error", "Test init failed for %s", currentTestName.c_str());229return false;230}231232System_Notify(SystemNotification::BOOT_DONE);233234Core_UpdateDebugStats((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS || g_Config.bLogFrameDrops);235236PSP_BeginHostFrame();237Draw::DrawContext *draw = coreParameter.graphicsContext ? coreParameter.graphicsContext->GetDrawContext() : nullptr;238if (draw)239draw->BeginFrame(Draw::DebugFlags::NONE);240241bool passed = true;242double deadline = time_now_d() + opt.timeout;243coreState = coreParameter.startBreak ? CORE_STEPPING : CORE_RUNNING;244while (coreState == CORE_RUNNING || coreState == CORE_STEPPING)245{246int blockTicks = (int)usToCycles(1000000 / 10);247PSP_RunLoopFor(blockTicks);248249// If we were rendering, this might be a nice time to do something about it.250if (coreState == CORE_NEXTFRAME) {251coreState = CORE_RUNNING;252headlessHost->SwapBuffers();253}254if (coreState == CORE_STEPPING && !coreParameter.startBreak) {255break;256}257if (time_now_d() > deadline) {258// Don't compare, print the output at least up to this point, and bail.259if (!opt.bench) {260printf("%s", output.c_str());261262System_SendDebugOutput("TIMEOUT\n");263TeamCityPrint("testFailed name='%s' message='Test timeout'", currentTestName.c_str());264GitHubActionsPrint("error", "Test timeout for %s", currentTestName.c_str());265}266267passed = false;268Core_Stop();269}270}271PSP_EndHostFrame();272273if (draw) {274draw->BindFramebufferAsRenderTarget(nullptr, { Draw::RPAction::CLEAR, Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE }, "Headless");275// Vulkan may get angry if we don't do a final present.276if (gpu)277gpu->CopyDisplayToOutput(true);278279draw->EndFrame();280}281282PSP_Shutdown();283284if (!opt.bench)285headlessHost->FlushDebugOutput();286287if (opt.compare && passed)288passed = CompareOutput(coreParameter.fileToStart, output, opt.verbose);289290TeamCityPrint("testFinished name='%s'", currentTestName.c_str());291292return passed;293}294295std::vector<std::string> ReadFromListFile(const std::string &listFilename) {296std::vector<std::string> testFilenames;297char temp[2048]{};298299if (listFilename == "-") {300while (scanf("%2047s", temp) == 1)301testFilenames.push_back(temp);302} else {303FILE *fp = File::OpenCFile(Path(listFilename), "rt");304if (!fp) {305fprintf(stderr, "Unable to open '%s' as a list file\n", listFilename.c_str());306return testFilenames;307}308309while (fscanf(fp, "%2047s", temp) == 1)310testFilenames.push_back(temp);311fclose(fp);312}313314return testFilenames;315}316317int main(int argc, const char* argv[])318{319PROFILE_INIT();320TimeInit();321#if PPSSPP_PLATFORM(WINDOWS)322SetCleanExitOnAssert();323#else324// Ignore sigpipe.325if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {326perror("Unable to ignore SIGPIPE");327}328#endif329330#if defined(_DEBUG) && defined(_MSC_VER)331_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);332#endif333334AutoTestOptions testOptions{};335testOptions.timeout = std::numeric_limits<double>::infinity();336bool fullLog = false;337const char *stateToLoad = 0;338GPUCore gpuCore = GPUCORE_SOFTWARE;339CPUCore cpuCore = CPUCore::JIT;340int debuggerPort = -1;341bool newAtrac = false;342343std::vector<std::string> testFilenames;344const char *mountIso = nullptr;345const char *mountRoot = nullptr;346const char *screenshotFilename = nullptr;347348for (int i = 1; i < argc; i++)349{350if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--mount"))351{352if (++i >= argc)353return printUsage(argv[0], "Missing argument after -m");354mountIso = argv[i];355}356else if (!strcmp(argv[i], "-r") || !strcmp(argv[i], "--root"))357{358if (++i >= argc)359return printUsage(argv[0], "Missing argument after -r");360mountRoot = argv[i];361}362else if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--log"))363fullLog = true;364else if (!strcmp(argv[i], "-i"))365cpuCore = CPUCore::INTERPRETER;366else if (!strcmp(argv[i], "-j"))367cpuCore = CPUCore::JIT;368else if (!strcmp(argv[i], "--jit-ir"))369cpuCore = CPUCore::JIT_IR;370else if (!strcmp(argv[i], "--ir"))371cpuCore = CPUCore::IR_INTERPRETER;372else if (!strcmp(argv[i], "-c") || !strcmp(argv[i], "--compare"))373testOptions.compare = true;374else if (!strcmp(argv[i], "--bench"))375testOptions.bench = true;376else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose"))377testOptions.verbose = true;378else if (!strcmp(argv[i], "--new-atrac"))379newAtrac = true;380else if (!strncmp(argv[i], "--graphics=", strlen("--graphics=")) && strlen(argv[i]) > strlen("--graphics="))381{382const char *gpuName = argv[i] + strlen("--graphics=");383if (!strcasecmp(gpuName, "gles"))384gpuCore = GPUCORE_GLES;385// There used to be a separate "null" rendering core - just use software.386else if (!strcasecmp(gpuName, "software") || !strcasecmp(gpuName, "null"))387gpuCore = GPUCORE_SOFTWARE;388else if (!strcasecmp(gpuName, "directx9"))389gpuCore = GPUCORE_DIRECTX9;390else if (!strcasecmp(gpuName, "directx11"))391gpuCore = GPUCORE_DIRECTX11;392else if (!strcasecmp(gpuName, "vulkan"))393gpuCore = GPUCORE_VULKAN;394else395return printUsage(argv[0], "Unknown gpu backend specified after --graphics=. Allowed: software, directx9, directx11, vulkan, gles, null.");396}397// Default to GLES if no value selected.398else if (!strcmp(argv[i], "--graphics")) {399#if PPSSPP_API(ANY_GL)400gpuCore = GPUCORE_GLES;401#else402gpuCore = GPUCORE_DIRECTX11;403#endif404} else if (!strncmp(argv[i], "--screenshot=", strlen("--screenshot=")) && strlen(argv[i]) > strlen("--screenshot="))405screenshotFilename = argv[i] + strlen("--screenshot=");406else if (!strncmp(argv[i], "--timeout=", strlen("--timeout=")) && strlen(argv[i]) > strlen("--timeout="))407testOptions.timeout = strtod(argv[i] + strlen("--timeout="), nullptr);408else if (!strncmp(argv[i], "--max-mse=", strlen("--max-mse=")) && strlen(argv[i]) > strlen("--max-mse="))409testOptions.maxScreenshotError = strtod(argv[i] + strlen("--max-mse="), nullptr);410else if (!strncmp(argv[i], "--debugger=", strlen("--debugger=")) && strlen(argv[i]) > strlen("--debugger="))411debuggerPort = (int)strtoul(argv[i] + strlen("--debugger="), NULL, 10);412else if (!strcmp(argv[i], "--teamcity"))413teamCityMode = true;414else if (!strncmp(argv[i], "--state=", strlen("--state=")) && strlen(argv[i]) > strlen("--state="))415stateToLoad = argv[i] + strlen("--state=");416else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h"))417return printUsage(argv[0], NULL);418else419testFilenames.push_back(argv[i]);420}421422if (testFilenames.size() == 1 && testFilenames[0][0] == '@')423testFilenames = ReadFromListFile(testFilenames[0].substr(1));424425if (testFilenames.empty())426return printUsage(argv[0], argc <= 1 ? NULL : "No executables specified");427428LogManager::Init(&g_Config.bEnableLogging);429LogManager *logman = LogManager::GetInstance();430431PrintfLogger *printfLogger = new PrintfLogger();432433for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; i++) {434Log type = (Log)i;435logman->SetEnabled(type, fullLog);436logman->SetLogLevel(type, LogLevel::LDEBUG);437}438logman->AddListener(printfLogger);439440// Needs to be after log so we don't interfere with test output.441g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count);442443HeadlessHost *headlessHost = getHost(gpuCore);444g_headlessHost = headlessHost;445446std::string error_string;447GraphicsContext *graphicsContext = nullptr;448bool glWorking = headlessHost->InitGraphics(&error_string, &graphicsContext, gpuCore);449450CoreParameter coreParameter;451coreParameter.cpuCore = cpuCore;452coreParameter.gpuCore = glWorking ? gpuCore : GPUCORE_SOFTWARE;453coreParameter.graphicsContext = graphicsContext;454coreParameter.enableSound = false;455coreParameter.mountIso = mountIso ? Path(std::string(mountIso)) : Path();456coreParameter.mountRoot = mountRoot ? Path(std::string(mountRoot)) : Path();457coreParameter.startBreak = false;458coreParameter.headLess = true;459coreParameter.renderScaleFactor = 1;460coreParameter.renderWidth = 480;461coreParameter.renderHeight = 272;462coreParameter.pixelWidth = 480;463coreParameter.pixelHeight = 272;464coreParameter.fastForward = true;465466g_Config.bEnableSound = false;467g_Config.bFirstRun = false;468g_Config.bIgnoreBadMemAccess = true;469// Never report from tests.470g_Config.sReportHost.clear();471g_Config.bAutoSaveSymbolMap = false;472g_Config.bSkipBufferEffects = false;473g_Config.iSkipGPUReadbackMode = (int)SkipGPUReadbackMode::NO_SKIP;474g_Config.bHardwareTransform = true;475g_Config.iAnisotropyLevel = 0; // When testing mipmapping we really don't want this.476g_Config.iMultiSampleLevel = 0;477g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_ENGLISH;478g_Config.iTimeFormat = PSP_SYSTEMPARAM_TIME_FORMAT_24HR;479g_Config.bEncryptSave = true;480g_Config.sNickName = "shadow";481g_Config.iTimeZone = 60;482g_Config.iDateFormat = PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY;483g_Config.iButtonPreference = PSP_SYSTEMPARAM_BUTTON_CROSS;484g_Config.iLockParentalLevel = 9;485g_Config.iInternalResolution = 1;486g_Config.iFastForwardMode = (int)FastForwardMode::CONTINUOUS;487g_Config.bEnableLogging = fullLog;488g_Config.bSoftwareSkinning = true;489g_Config.bVertexDecoderJit = true;490g_Config.bSoftwareRendering = coreParameter.gpuCore == GPUCORE_SOFTWARE;491g_Config.bSoftwareRenderingJit = true;492g_Config.iSplineBezierQuality = 2;493g_Config.bHighQualityDepth = true;494g_Config.bMemStickInserted = true;495g_Config.iMemStickSizeGB = 16;496g_Config.bEnableWlan = true;497g_Config.sMACAddress = "12:34:56:78:9A:BC";498g_Config.iFirmwareVersion = PSP_DEFAULT_FIRMWARE;499g_Config.iPSPModel = PSP_MODEL_SLIM;500g_Config.iGlobalVolume = VOLUME_FULL;501g_Config.iReverbVolume = VOLUME_FULL;502g_Config.internalDataDirectory.clear();503g_Config.bUseNewAtrac = newAtrac;504505Path exePath = File::GetExeDirectory();506g_Config.flash0Directory = exePath / "assets/flash0";507508#if PPSSPP_PLATFORM(WINDOWS)509// Mount a filesystem510g_Config.memStickDirectory = exePath / "memstick";511File::CreateDir(g_Config.memStickDirectory);512CreateSysDirectories();513#elif !PPSSPP_PLATFORM(ANDROID)514g_Config.memStickDirectory = Path(std::string(getenv("HOME"))) / ".ppsspp";515#endif516517// Try to find the flash0 directory. Often this is from a subdirectory.518Path nextPath = exePath;519for (int i = 0; i < 5; ++i) {520if (File::Exists(nextPath / "assets/flash0")) {521g_Config.flash0Directory = nextPath / "assets/flash0";522#if !PPSSPP_PLATFORM(ANDROID)523g_VFS.Register("", new DirectoryReader(nextPath / "assets"));524#endif525break;526}527528if (!nextPath.CanNavigateUp())529break;530nextPath = nextPath.NavigateUp();531}532533if (screenshotFilename)534headlessHost->SetComparisonScreenshot(Path(std::string(screenshotFilename)), testOptions.maxScreenshotError);535headlessHost->SetWriteFailureScreenshot(!teamCityMode && !getenv("GITHUB_ACTIONS") && !testOptions.bench);536headlessHost->SetWriteDebugOutput(!testOptions.compare && !testOptions.bench);537538#if PPSSPP_PLATFORM(ANDROID)539// For some reason the debugger installs it with this name?540if (File::Exists(Path("/data/app/org.ppsspp.ppsspp-2.apk"))) {541g_VFS.Register("", ZipFileReader::Create(Path("/data/app/org.ppsspp.ppsspp-2.apk"), "assets/"));542}543if (File::Exists(Path("/data/app/org.ppsspp.ppsspp.apk"))) {544g_VFS.Register("", ZipFileReader::Create(Path("/data/app/org.ppsspp.ppsspp.apk"), "assets/"));545}546#elif PPSSPP_PLATFORM(LINUX)547g_VFS.Register("", new DirectoryReader(Path("/usr/local/share/ppsspp/assets")));548g_VFS.Register("", new DirectoryReader(Path("/usr/local/share/games/ppsspp/assets")));549g_VFS.Register("", new DirectoryReader(Path("/usr/share/ppsspp/assets")));550g_VFS.Register("", new DirectoryReader(Path("/usr/share/games/ppsspp/assets")));551#endif552553UpdateUIState(UISTATE_INGAME);554555if (debuggerPort > 0) {556g_Config.iRemoteISOPort = debuggerPort;557coreParameter.startBreak = true;558StartWebServer(WebServerFlags::DEBUGGER);559}560561if (stateToLoad != NULL)562SaveState::Load(Path(stateToLoad), -1);563564std::vector<std::string> failedTests;565std::vector<std::string> passedTests;566for (size_t i = 0; i < testFilenames.size(); ++i)567{568coreParameter.fileToStart = Path(testFilenames[i]);569if (testOptions.compare)570printf("%s:\n", coreParameter.fileToStart.c_str());571bool passed = RunAutoTest(headlessHost, coreParameter, testOptions);572if (testOptions.bench) {573double st = time_now_d();574double deadline = st + testOptions.timeout;575double runs = 0.0;576for (int i = 0; i < 100; ++i) {577RunAutoTest(headlessHost, coreParameter, testOptions);578runs++;579580if (time_now_d() > deadline)581break;582}583double et = time_now_d();584585std::string testName = GetTestName(coreParameter.fileToStart);586printf(" %s - %f seconds average\n", testName.c_str(), (et - st) / runs);587}588if (testOptions.compare) {589std::string testName = GetTestName(coreParameter.fileToStart);590if (passed) {591passedTests.push_back(testName);592printf(" %s - passed!\n", testName.c_str());593}594else595failedTests.push_back(testName);596}597}598599if (testOptions.compare) {600printf("%d tests passed, %d tests failed.\n", (int)passedTests.size(), (int)failedTests.size());601if (!failedTests.empty())602{603printf("Failed tests:\n");604for (size_t i = 0; i < failedTests.size(); ++i) {605printf(" %s\n", failedTests[i].c_str());606}607}608}609610if (debuggerPort > 0) {611ShutdownWebServer();612}613614headlessHost->ShutdownGraphics();615delete headlessHost;616headlessHost = nullptr;617g_headlessHost = nullptr;618619g_VFS.Clear();620LogManager::Shutdown();621delete printfLogger;622623#if PPSSPP_PLATFORM(WINDOWS)624timeEndPeriod(1);625#endif626627g_threadManager.Teardown();628629if (!failedTests.empty() && !teamCityMode)630return 1;631return 0;632}633634635