main.cpp 195 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126
  1. // SPDX-FileCopyrightText: 2014 Citra Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <cinttypes>
  4. #include <clocale>
  5. #include <cmath>
  6. #include <fstream>
  7. #include <iostream>
  8. #include <memory>
  9. #include <thread>
  10. #include "core/hle/service/am/applet_manager.h"
  11. #include "core/loader/nca.h"
  12. #include "core/tools/renderdoc.h"
  13. #ifdef __APPLE__
  14. #include <unistd.h> // for chdir
  15. #endif
  16. #ifdef __unix__
  17. #include <csignal>
  18. #include <sys/socket.h>
  19. #include "common/linux/gamemode.h"
  20. #endif
  21. #include <boost/container/flat_set.hpp>
  22. // VFS includes must be before glad as they will conflict with Windows file api, which uses defines.
  23. #include "applets/qt_amiibo_settings.h"
  24. #include "applets/qt_controller.h"
  25. #include "applets/qt_error.h"
  26. #include "applets/qt_profile_select.h"
  27. #include "applets/qt_software_keyboard.h"
  28. #include "applets/qt_web_browser.h"
  29. #include "common/nvidia_flags.h"
  30. #include "common/settings_enums.h"
  31. #include "configuration/configure_input.h"
  32. #include "configuration/configure_per_game.h"
  33. #include "configuration/configure_tas.h"
  34. #include "core/file_sys/romfs_factory.h"
  35. #include "core/file_sys/vfs/vfs.h"
  36. #include "core/file_sys/vfs/vfs_real.h"
  37. #include "core/frontend/applets/cabinet.h"
  38. #include "core/frontend/applets/controller.h"
  39. #include "core/frontend/applets/general.h"
  40. #include "core/frontend/applets/mii_edit.h"
  41. #include "core/frontend/applets/software_keyboard.h"
  42. #include "core/hle/service/acc/profile_manager.h"
  43. #include "core/hle/service/am/frontend/applets.h"
  44. #include "core/hle/service/set/system_settings_server.h"
  45. #include "frontend_common/content_manager.h"
  46. #include "hid_core/frontend/emulated_controller.h"
  47. #include "hid_core/hid_core.h"
  48. #include "yuzu/multiplayer/state.h"
  49. #include "yuzu/util/controller_navigation.h"
  50. // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows
  51. // defines.
  52. static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper(
  53. const FileSys::VirtualFilesystem& vfs, const std::string& path, FileSys::OpenMode mode) {
  54. return vfs->CreateDirectory(path, mode);
  55. }
  56. static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::VirtualDir& dir,
  57. const std::string& path) {
  58. return dir->CreateFile(path);
  59. }
  60. #include <fmt/ostream.h>
  61. #include <glad/glad.h>
  62. #define QT_NO_OPENGL
  63. #include <QClipboard>
  64. #include <QDesktopServices>
  65. #include <QDir>
  66. #include <QFile>
  67. #include <QFileDialog>
  68. #include <QInputDialog>
  69. #include <QMessageBox>
  70. #include <QProgressBar>
  71. #include <QProgressDialog>
  72. #include <QPushButton>
  73. #include <QScreen>
  74. #include <QShortcut>
  75. #include <QStandardPaths>
  76. #include <QStatusBar>
  77. #include <QString>
  78. #include <QSysInfo>
  79. #include <QUrl>
  80. #include <QtConcurrent/QtConcurrent>
  81. #ifdef HAVE_SDL2
  82. #include <SDL.h> // For SDL ScreenSaver functions
  83. #endif
  84. #include <fmt/format.h>
  85. #include "common/detached_tasks.h"
  86. #include "common/fs/fs.h"
  87. #include "common/fs/path_util.h"
  88. #include "common/literals.h"
  89. #include "common/logging/backend.h"
  90. #include "common/logging/log.h"
  91. #include "common/memory_detect.h"
  92. #include "common/microprofile.h"
  93. #include "common/scm_rev.h"
  94. #include "common/scope_exit.h"
  95. #ifdef _WIN32
  96. #include <shlobj.h>
  97. #include "common/windows/timer_resolution.h"
  98. #endif
  99. #ifdef ARCHITECTURE_x86_64
  100. #include "common/x64/cpu_detect.h"
  101. #endif
  102. #include "common/settings.h"
  103. #include "common/telemetry.h"
  104. #include "core/core.h"
  105. #include "core/core_timing.h"
  106. #include "core/crypto/key_manager.h"
  107. #include "core/file_sys/card_image.h"
  108. #include "core/file_sys/common_funcs.h"
  109. #include "core/file_sys/content_archive.h"
  110. #include "core/file_sys/control_metadata.h"
  111. #include "core/file_sys/patch_manager.h"
  112. #include "core/file_sys/registered_cache.h"
  113. #include "core/file_sys/romfs.h"
  114. #include "core/file_sys/savedata_factory.h"
  115. #include "core/file_sys/submission_package.h"
  116. #include "core/hle/kernel/k_process.h"
  117. #include "core/hle/service/am/am.h"
  118. #include "core/hle/service/filesystem/filesystem.h"
  119. #include "core/hle/service/sm/sm.h"
  120. #include "core/loader/loader.h"
  121. #include "core/perf_stats.h"
  122. #include "core/telemetry_session.h"
  123. #include "frontend_common/config.h"
  124. #include "input_common/drivers/tas_input.h"
  125. #include "input_common/drivers/virtual_amiibo.h"
  126. #include "input_common/main.h"
  127. #include "ui_main.h"
  128. #include "util/overlay_dialog.h"
  129. #include "video_core/gpu.h"
  130. #include "video_core/renderer_base.h"
  131. #include "video_core/shader_notify.h"
  132. #include "yuzu/about_dialog.h"
  133. #include "yuzu/bootmanager.h"
  134. #include "yuzu/compatdb.h"
  135. #include "yuzu/compatibility_list.h"
  136. #include "yuzu/configuration/configure_dialog.h"
  137. #include "yuzu/configuration/configure_input_per_game.h"
  138. #include "yuzu/configuration/qt_config.h"
  139. #include "yuzu/debugger/console.h"
  140. #include "yuzu/debugger/controller.h"
  141. #include "yuzu/debugger/profiler.h"
  142. #include "yuzu/debugger/wait_tree.h"
  143. #include "yuzu/discord.h"
  144. #include "yuzu/game_list.h"
  145. #include "yuzu/game_list_p.h"
  146. #include "yuzu/hotkeys.h"
  147. #include "yuzu/install_dialog.h"
  148. #include "yuzu/loading_screen.h"
  149. #include "yuzu/main.h"
  150. #include "yuzu/play_time_manager.h"
  151. #include "yuzu/startup_checks.h"
  152. #include "yuzu/uisettings.h"
  153. #include "yuzu/util/clickable_label.h"
  154. #include "yuzu/vk_device_info.h"
  155. #ifdef YUZU_CRASH_DUMPS
  156. #include "yuzu/breakpad.h"
  157. #endif
  158. using namespace Common::Literals;
  159. #ifdef USE_DISCORD_PRESENCE
  160. #include "yuzu/discord_impl.h"
  161. #endif
  162. #ifdef QT_STATICPLUGIN
  163. Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
  164. #endif
  165. #ifdef _WIN32
  166. #include <windows.h>
  167. extern "C" {
  168. // tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable
  169. // graphics
  170. __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
  171. __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
  172. }
  173. #endif
  174. constexpr int default_mouse_hide_timeout = 2500;
  175. constexpr int default_input_update_timeout = 1;
  176. constexpr size_t CopyBufferSize = 1_MiB;
  177. /**
  178. * "Callouts" are one-time instructional messages shown to the user. In the config settings, there
  179. * is a bitfield "callout_flags" options, used to track if a message has already been shown to the
  180. * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones.
  181. */
  182. enum class CalloutFlag : uint32_t {
  183. Telemetry = 0x1,
  184. DRDDeprecation = 0x2,
  185. };
  186. void GMainWindow::ShowTelemetryCallout() {
  187. if (UISettings::values.callout_flags.GetValue() &
  188. static_cast<uint32_t>(CalloutFlag::Telemetry)) {
  189. return;
  190. }
  191. UISettings::values.callout_flags =
  192. UISettings::values.callout_flags.GetValue() | static_cast<uint32_t>(CalloutFlag::Telemetry);
  193. const QString telemetry_message =
  194. tr("<a href='https://yuzu-emu.org/help/feature/telemetry/'>Anonymous "
  195. "data is collected</a> to help improve yuzu. "
  196. "<br/><br/>Would you like to share your usage data with us?");
  197. if (!question(this, tr("Telemetry"), telemetry_message)) {
  198. Settings::values.enable_telemetry = false;
  199. system->ApplySettings();
  200. }
  201. }
  202. const int GMainWindow::max_recent_files_item;
  203. static void RemoveCachedContents() {
  204. const auto cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir);
  205. const auto offline_fonts = cache_dir / "fonts";
  206. const auto offline_manual = cache_dir / "offline_web_applet_manual";
  207. const auto offline_legal_information = cache_dir / "offline_web_applet_legal_information";
  208. const auto offline_system_data = cache_dir / "offline_web_applet_system_data";
  209. Common::FS::RemoveDirRecursively(offline_fonts);
  210. Common::FS::RemoveDirRecursively(offline_manual);
  211. Common::FS::RemoveDirRecursively(offline_legal_information);
  212. Common::FS::RemoveDirRecursively(offline_system_data);
  213. }
  214. static void LogRuntimes() {
  215. #ifdef _MSC_VER
  216. // It is possible that the name of the dll will change.
  217. // vcruntime140.dll is for 2015 and onwards
  218. static constexpr char runtime_dll_name[] = "vcruntime140.dll";
  219. UINT sz = GetFileVersionInfoSizeA(runtime_dll_name, nullptr);
  220. bool runtime_version_inspection_worked = false;
  221. if (sz > 0) {
  222. std::vector<u8> buf(sz);
  223. if (GetFileVersionInfoA(runtime_dll_name, 0, sz, buf.data())) {
  224. VS_FIXEDFILEINFO* pvi;
  225. sz = sizeof(VS_FIXEDFILEINFO);
  226. if (VerQueryValueA(buf.data(), "\\", reinterpret_cast<LPVOID*>(&pvi), &sz)) {
  227. if (pvi->dwSignature == VS_FFI_SIGNATURE) {
  228. runtime_version_inspection_worked = true;
  229. LOG_INFO(Frontend, "MSVC Compiler: {} Runtime: {}.{}.{}.{}", _MSC_VER,
  230. pvi->dwProductVersionMS >> 16, pvi->dwProductVersionMS & 0xFFFF,
  231. pvi->dwProductVersionLS >> 16, pvi->dwProductVersionLS & 0xFFFF);
  232. }
  233. }
  234. }
  235. }
  236. if (!runtime_version_inspection_worked) {
  237. LOG_INFO(Frontend, "Unable to inspect {}", runtime_dll_name);
  238. }
  239. #endif
  240. LOG_INFO(Frontend, "Qt Compile: {} Runtime: {}", QT_VERSION_STR, qVersion());
  241. }
  242. static QString PrettyProductName() {
  243. #ifdef _WIN32
  244. // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2
  245. // With that notation change they changed the registry key used to denote the current version
  246. QSettings windows_registry(
  247. QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"),
  248. QSettings::NativeFormat);
  249. const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString();
  250. if (release_id == QStringLiteral("2009")) {
  251. const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt();
  252. const QString display_version =
  253. windows_registry.value(QStringLiteral("DisplayVersion")).toString();
  254. const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt();
  255. u32 version = 10;
  256. if (current_build >= 22000) {
  257. version = 11;
  258. }
  259. return QStringLiteral("Windows %1 Version %2 (Build %3.%4)")
  260. .arg(QString::number(version), display_version, QString::number(current_build),
  261. QString::number(ubr));
  262. }
  263. #endif
  264. return QSysInfo::prettyProductName();
  265. }
  266. #ifdef _WIN32
  267. static void OverrideWindowsFont() {
  268. // Qt5 chooses these fonts on Windows and they have fairly ugly alphanumeric/cyrillic characters
  269. // Asking to use "MS Shell Dlg 2" gives better other chars while leaving the Chinese Characters.
  270. const QString startup_font = QApplication::font().family();
  271. const QStringList ugly_fonts = {QStringLiteral("SimSun"), QStringLiteral("PMingLiU")};
  272. if (ugly_fonts.contains(startup_font)) {
  273. QApplication::setFont(QFont(QStringLiteral("MS Shell Dlg 2"), 9, QFont::Normal));
  274. }
  275. }
  276. #endif
  277. bool GMainWindow::CheckDarkMode() {
  278. #ifdef __unix__
  279. const QPalette test_palette(qApp->palette());
  280. const QColor text_color = test_palette.color(QPalette::Active, QPalette::Text);
  281. const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window);
  282. return (text_color.value() > window_color.value());
  283. #else
  284. // TODO: Windows
  285. return false;
  286. #endif // __unix__
  287. }
  288. GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan)
  289. : ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()},
  290. input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::move(config_)},
  291. vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
  292. provider{std::make_unique<FileSys::ManualContentProvider>()} {
  293. #ifdef __unix__
  294. SetupSigInterrupts();
  295. SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
  296. #endif
  297. system->Initialize();
  298. Common::Log::Initialize();
  299. Common::Log::Start();
  300. LoadTranslation();
  301. setAcceptDrops(true);
  302. ui->setupUi(this);
  303. statusBar()->hide();
  304. // Check dark mode before a theme is loaded
  305. os_dark_mode = CheckDarkMode();
  306. startup_icon_theme = QIcon::themeName();
  307. // fallback can only be set once, colorful theme icons are okay on both light/dark
  308. QIcon::setFallbackThemeName(QStringLiteral("colorful"));
  309. QIcon::setFallbackSearchPaths(QStringList(QStringLiteral(":/icons")));
  310. default_theme_paths = QIcon::themeSearchPaths();
  311. UpdateUITheme();
  312. SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
  313. discord_rpc->Update();
  314. play_time_manager = std::make_unique<PlayTime::PlayTimeManager>(system->GetProfileManager());
  315. system->GetRoomNetwork().Init();
  316. RegisterMetaTypes();
  317. InitializeWidgets();
  318. InitializeDebugWidgets();
  319. InitializeRecentFileMenuActions();
  320. InitializeHotkeys();
  321. SetDefaultUIGeometry();
  322. RestoreUIState();
  323. ConnectMenuEvents();
  324. ConnectWidgetEvents();
  325. system->HIDCore().ReloadInputDevices();
  326. controller_dialog->refreshConfiguration();
  327. const auto branch_name = std::string(Common::g_scm_branch);
  328. const auto description = std::string(Common::g_scm_desc);
  329. const auto build_id = std::string(Common::g_build_id);
  330. const auto yuzu_build = fmt::format("yuzu Development Build | {}-{}", branch_name, description);
  331. const auto override_build =
  332. fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id);
  333. const auto yuzu_build_version = override_build.empty() ? yuzu_build : override_build;
  334. const auto processor_count = std::thread::hardware_concurrency();
  335. LOG_INFO(Frontend, "yuzu Version: {}", yuzu_build_version);
  336. LogRuntimes();
  337. #ifdef ARCHITECTURE_x86_64
  338. const auto& caps = Common::GetCPUCaps();
  339. std::string cpu_string = caps.cpu_string;
  340. if (caps.avx || caps.avx2 || caps.avx512f) {
  341. cpu_string += " | AVX";
  342. if (caps.avx512f) {
  343. cpu_string += "512";
  344. } else if (caps.avx2) {
  345. cpu_string += '2';
  346. }
  347. if (caps.fma || caps.fma4) {
  348. cpu_string += " | FMA";
  349. }
  350. }
  351. LOG_INFO(Frontend, "Host CPU: {}", cpu_string);
  352. if (std::optional<int> processor_core = Common::GetProcessorCount()) {
  353. LOG_INFO(Frontend, "Host CPU Cores: {}", *processor_core);
  354. }
  355. #endif
  356. LOG_INFO(Frontend, "Host CPU Threads: {}", processor_count);
  357. LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString());
  358. LOG_INFO(Frontend, "Host RAM: {:.2f} GiB",
  359. Common::GetMemInfo().TotalPhysicalMemory / f64{1_GiB});
  360. LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", Common::GetMemInfo().TotalSwapMemory / f64{1_GiB});
  361. #ifdef _WIN32
  362. LOG_INFO(Frontend, "Host Timer Resolution: {:.4f} ms",
  363. std::chrono::duration_cast<std::chrono::duration<f64, std::milli>>(
  364. Common::Windows::SetCurrentTimerResolutionToMaximum())
  365. .count());
  366. system->CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution());
  367. #endif
  368. UpdateWindowTitle();
  369. show();
  370. system->SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
  371. system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
  372. provider.get());
  373. system->GetFileSystemController().CreateFactories(*vfs);
  374. // Remove cached contents generated during the previous session
  375. RemoveCachedContents();
  376. // Gen keys if necessary
  377. OnCheckFirmwareDecryption();
  378. game_list->LoadCompatibilityList();
  379. game_list->PopulateAsync(UISettings::values.game_dirs);
  380. // Show one-time "callout" messages to the user
  381. ShowTelemetryCallout();
  382. // make sure menubar has the arrow cursor instead of inheriting from this
  383. ui->menubar->setCursor(QCursor());
  384. statusBar()->setCursor(QCursor());
  385. mouse_hide_timer.setInterval(default_mouse_hide_timeout);
  386. connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
  387. connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
  388. update_input_timer.setInterval(default_input_update_timeout);
  389. connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
  390. update_input_timer.start();
  391. MigrateConfigFiles();
  392. if (has_broken_vulkan) {
  393. UISettings::values.has_broken_vulkan = true;
  394. QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"),
  395. tr("Vulkan initialization failed during boot.<br><br>Click <a "
  396. "href='https://yuzu-emu.org/wiki/faq/"
  397. "#yuzu-starts-with-the-error-broken-vulkan-installation-detected'>"
  398. "here for instructions to fix the issue</a>."));
  399. #ifdef HAS_OPENGL
  400. Settings::values.renderer_backend = Settings::RendererBackend::OpenGL;
  401. #else
  402. Settings::values.renderer_backend = Settings::RendererBackend::Null;
  403. #endif
  404. UpdateAPIText();
  405. renderer_status_button->setDisabled(true);
  406. renderer_status_button->setChecked(false);
  407. } else {
  408. VkDeviceInfo::PopulateRecords(vk_device_records, this->window()->windowHandle());
  409. }
  410. #if defined(HAVE_SDL2) && !defined(_WIN32)
  411. SDL_InitSubSystem(SDL_INIT_VIDEO);
  412. // Set a screensaver inhibition reason string. Currently passed to DBus by SDL and visible to
  413. // the user through their desktop environment.
  414. //: TRANSLATORS: This string is shown to the user to explain why yuzu needs to prevent the
  415. //: computer from sleeping
  416. QByteArray wakelock_reason = tr("Running a game").toUtf8();
  417. SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, wakelock_reason.data());
  418. // SDL disables the screen saver by default, and setting the hint
  419. // SDL_HINT_VIDEO_ALLOW_SCREENSAVER doesn't seem to work, so we just enable the screen saver
  420. // for now.
  421. SDL_EnableScreenSaver();
  422. #endif
  423. SetupPrepareForSleep();
  424. QStringList args = QApplication::arguments();
  425. if (args.size() < 2) {
  426. return;
  427. }
  428. QString game_path;
  429. bool has_gamepath = false;
  430. bool is_fullscreen = false;
  431. for (int i = 1; i < args.size(); ++i) {
  432. // Preserves drag/drop functionality
  433. if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) {
  434. game_path = args[1];
  435. has_gamepath = true;
  436. break;
  437. }
  438. // Launch game in fullscreen mode
  439. if (args[i] == QStringLiteral("-f")) {
  440. is_fullscreen = true;
  441. continue;
  442. }
  443. // Launch game with a specific user
  444. if (args[i] == QStringLiteral("-u")) {
  445. if (i >= args.size() - 1) {
  446. continue;
  447. }
  448. if (args[i + 1].startsWith(QChar::fromLatin1('-'))) {
  449. continue;
  450. }
  451. int user_arg_idx = ++i;
  452. bool argument_ok;
  453. std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok);
  454. if (!argument_ok) {
  455. // try to look it up by username, only finds the first username that matches.
  456. const std::string user_arg_str = args[user_arg_idx].toStdString();
  457. const auto user_idx = system->GetProfileManager().GetUserIndex(user_arg_str);
  458. if (user_idx == std::nullopt) {
  459. LOG_ERROR(Frontend, "Invalid user argument");
  460. continue;
  461. }
  462. selected_user = user_idx.value();
  463. }
  464. if (!system->GetProfileManager().UserExistsIndex(selected_user)) {
  465. LOG_ERROR(Frontend, "Selected user doesn't exist");
  466. continue;
  467. }
  468. Settings::values.current_user = static_cast<s32>(selected_user);
  469. user_flag_cmd_line = true;
  470. continue;
  471. }
  472. // Launch game at path
  473. if (args[i] == QStringLiteral("-g")) {
  474. if (i >= args.size() - 1) {
  475. continue;
  476. }
  477. if (args[i + 1].startsWith(QChar::fromLatin1('-'))) {
  478. continue;
  479. }
  480. game_path = args[++i];
  481. has_gamepath = true;
  482. }
  483. }
  484. // Override fullscreen setting if gamepath or argument is provided
  485. if (has_gamepath || is_fullscreen) {
  486. ui->action_Fullscreen->setChecked(is_fullscreen);
  487. }
  488. if (!game_path.isEmpty()) {
  489. BootGame(game_path, ApplicationAppletParameters());
  490. }
  491. }
  492. GMainWindow::~GMainWindow() {
  493. // will get automatically deleted otherwise
  494. if (render_window->parent() == nullptr) {
  495. delete render_window;
  496. }
  497. #ifdef __unix__
  498. ::close(sig_interrupt_fds[0]);
  499. ::close(sig_interrupt_fds[1]);
  500. #endif
  501. }
  502. void GMainWindow::RegisterMetaTypes() {
  503. // Register integral and floating point types
  504. qRegisterMetaType<u8>("u8");
  505. qRegisterMetaType<u16>("u16");
  506. qRegisterMetaType<u32>("u32");
  507. qRegisterMetaType<u64>("u64");
  508. qRegisterMetaType<u128>("u128");
  509. qRegisterMetaType<s8>("s8");
  510. qRegisterMetaType<s16>("s16");
  511. qRegisterMetaType<s32>("s32");
  512. qRegisterMetaType<s64>("s64");
  513. qRegisterMetaType<f32>("f32");
  514. qRegisterMetaType<f64>("f64");
  515. // Register string types
  516. qRegisterMetaType<std::string>("std::string");
  517. qRegisterMetaType<std::wstring>("std::wstring");
  518. qRegisterMetaType<std::u8string>("std::u8string");
  519. qRegisterMetaType<std::u16string>("std::u16string");
  520. qRegisterMetaType<std::u32string>("std::u32string");
  521. qRegisterMetaType<std::string_view>("std::string_view");
  522. qRegisterMetaType<std::wstring_view>("std::wstring_view");
  523. qRegisterMetaType<std::u8string_view>("std::u8string_view");
  524. qRegisterMetaType<std::u16string_view>("std::u16string_view");
  525. qRegisterMetaType<std::u32string_view>("std::u32string_view");
  526. // Register applet types
  527. // Cabinet Applet
  528. qRegisterMetaType<Core::Frontend::CabinetParameters>("Core::Frontend::CabinetParameters");
  529. qRegisterMetaType<std::shared_ptr<Service::NFC::NfcDevice>>(
  530. "std::shared_ptr<Service::NFC::NfcDevice>");
  531. // Controller Applet
  532. qRegisterMetaType<Core::Frontend::ControllerParameters>("Core::Frontend::ControllerParameters");
  533. // Profile Select Applet
  534. qRegisterMetaType<Core::Frontend::ProfileSelectParameters>(
  535. "Core::Frontend::ProfileSelectParameters");
  536. // Software Keyboard Applet
  537. qRegisterMetaType<Core::Frontend::KeyboardInitializeParameters>(
  538. "Core::Frontend::KeyboardInitializeParameters");
  539. qRegisterMetaType<Core::Frontend::InlineAppearParameters>(
  540. "Core::Frontend::InlineAppearParameters");
  541. qRegisterMetaType<Core::Frontend::InlineTextParameters>("Core::Frontend::InlineTextParameters");
  542. qRegisterMetaType<Service::AM::Frontend::SwkbdResult>("Service::AM::Frontend::SwkbdResult");
  543. qRegisterMetaType<Service::AM::Frontend::SwkbdTextCheckResult>(
  544. "Service::AM::Frontend::SwkbdTextCheckResult");
  545. qRegisterMetaType<Service::AM::Frontend::SwkbdReplyType>(
  546. "Service::AM::Frontend::SwkbdReplyType");
  547. // Web Browser Applet
  548. qRegisterMetaType<Service::AM::Frontend::WebExitReason>("Service::AM::Frontend::WebExitReason");
  549. // Register loader types
  550. qRegisterMetaType<Core::SystemResultStatus>("Core::SystemResultStatus");
  551. }
  552. void GMainWindow::AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters,
  553. std::shared_ptr<Service::NFC::NfcDevice> nfp_device) {
  554. cabinet_applet =
  555. new QtAmiiboSettingsDialog(this, parameters, input_subsystem.get(), nfp_device);
  556. SCOPE_EXIT({
  557. cabinet_applet->deleteLater();
  558. cabinet_applet = nullptr;
  559. });
  560. cabinet_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint |
  561. Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
  562. cabinet_applet->setWindowModality(Qt::WindowModal);
  563. if (cabinet_applet->exec() == QDialog::Rejected) {
  564. emit AmiiboSettingsFinished(false, {});
  565. return;
  566. }
  567. emit AmiiboSettingsFinished(true, cabinet_applet->GetName());
  568. }
  569. void GMainWindow::AmiiboSettingsRequestExit() {
  570. if (cabinet_applet) {
  571. cabinet_applet->reject();
  572. }
  573. }
  574. void GMainWindow::ControllerSelectorReconfigureControllers(
  575. const Core::Frontend::ControllerParameters& parameters) {
  576. controller_applet =
  577. new QtControllerSelectorDialog(this, parameters, input_subsystem.get(), *system);
  578. SCOPE_EXIT({
  579. controller_applet->deleteLater();
  580. controller_applet = nullptr;
  581. });
  582. controller_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint |
  583. Qt::WindowStaysOnTopHint | Qt::WindowTitleHint |
  584. Qt::WindowSystemMenuHint);
  585. controller_applet->setWindowModality(Qt::WindowModal);
  586. bool is_success = controller_applet->exec() != QDialog::Rejected;
  587. // Don't forget to apply settings.
  588. system->HIDCore().DisableAllControllerConfiguration();
  589. system->ApplySettings();
  590. config->SaveAllValues();
  591. UpdateStatusButtons();
  592. emit ControllerSelectorReconfigureFinished(is_success);
  593. }
  594. void GMainWindow::ControllerSelectorRequestExit() {
  595. if (controller_applet) {
  596. controller_applet->reject();
  597. }
  598. }
  599. void GMainWindow::ProfileSelectorSelectProfile(
  600. const Core::Frontend::ProfileSelectParameters& parameters) {
  601. profile_select_applet = new QtProfileSelectionDialog(*system, this, parameters);
  602. SCOPE_EXIT({
  603. profile_select_applet->deleteLater();
  604. profile_select_applet = nullptr;
  605. });
  606. profile_select_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint |
  607. Qt::WindowStaysOnTopHint | Qt::WindowTitleHint |
  608. Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
  609. profile_select_applet->setWindowModality(Qt::WindowModal);
  610. if (profile_select_applet->exec() == QDialog::Rejected) {
  611. emit ProfileSelectorFinishedSelection(std::nullopt);
  612. return;
  613. }
  614. const auto uuid = system->GetProfileManager().GetUser(
  615. static_cast<std::size_t>(profile_select_applet->GetIndex()));
  616. if (!uuid.has_value()) {
  617. emit ProfileSelectorFinishedSelection(std::nullopt);
  618. return;
  619. }
  620. emit ProfileSelectorFinishedSelection(uuid);
  621. }
  622. void GMainWindow::ProfileSelectorRequestExit() {
  623. if (profile_select_applet) {
  624. profile_select_applet->reject();
  625. }
  626. }
  627. void GMainWindow::SoftwareKeyboardInitialize(
  628. bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters) {
  629. if (software_keyboard) {
  630. LOG_ERROR(Frontend, "The software keyboard is already initialized!");
  631. return;
  632. }
  633. software_keyboard = new QtSoftwareKeyboardDialog(render_window, *system, is_inline,
  634. std::move(initialize_parameters));
  635. if (is_inline) {
  636. connect(
  637. software_keyboard, &QtSoftwareKeyboardDialog::SubmitInlineText, this,
  638. [this](Service::AM::Frontend::SwkbdReplyType reply_type, std::u16string submitted_text,
  639. s32 cursor_position) {
  640. emit SoftwareKeyboardSubmitInlineText(reply_type, submitted_text, cursor_position);
  641. },
  642. Qt::QueuedConnection);
  643. } else {
  644. connect(
  645. software_keyboard, &QtSoftwareKeyboardDialog::SubmitNormalText, this,
  646. [this](Service::AM::Frontend::SwkbdResult result, std::u16string submitted_text,
  647. bool confirmed) {
  648. emit SoftwareKeyboardSubmitNormalText(result, submitted_text, confirmed);
  649. },
  650. Qt::QueuedConnection);
  651. }
  652. }
  653. void GMainWindow::SoftwareKeyboardShowNormal() {
  654. if (!software_keyboard) {
  655. LOG_ERROR(Frontend, "The software keyboard is not initialized!");
  656. return;
  657. }
  658. const auto& layout = render_window->GetFramebufferLayout();
  659. const auto x = layout.screen.left;
  660. const auto y = layout.screen.top;
  661. const auto w = layout.screen.GetWidth();
  662. const auto h = layout.screen.GetHeight();
  663. const auto scale_ratio = devicePixelRatioF();
  664. software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio),
  665. QSize(w, h) / scale_ratio);
  666. }
  667. void GMainWindow::SoftwareKeyboardShowTextCheck(
  668. Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
  669. std::u16string text_check_message) {
  670. if (!software_keyboard) {
  671. LOG_ERROR(Frontend, "The software keyboard is not initialized!");
  672. return;
  673. }
  674. software_keyboard->ShowTextCheckDialog(text_check_result, text_check_message);
  675. }
  676. void GMainWindow::SoftwareKeyboardShowInline(
  677. Core::Frontend::InlineAppearParameters appear_parameters) {
  678. if (!software_keyboard) {
  679. LOG_ERROR(Frontend, "The software keyboard is not initialized!");
  680. return;
  681. }
  682. const auto& layout = render_window->GetFramebufferLayout();
  683. const auto x =
  684. static_cast<int>(layout.screen.left + (0.5f * layout.screen.GetWidth() *
  685. ((2.0f * appear_parameters.key_top_translate_x) +
  686. (1.0f - appear_parameters.key_top_scale_x))));
  687. const auto y =
  688. static_cast<int>(layout.screen.top + (layout.screen.GetHeight() *
  689. ((2.0f * appear_parameters.key_top_translate_y) +
  690. (1.0f - appear_parameters.key_top_scale_y))));
  691. const auto w = static_cast<int>(layout.screen.GetWidth() * appear_parameters.key_top_scale_x);
  692. const auto h = static_cast<int>(layout.screen.GetHeight() * appear_parameters.key_top_scale_y);
  693. const auto scale_ratio = devicePixelRatioF();
  694. software_keyboard->ShowInlineKeyboard(std::move(appear_parameters),
  695. render_window->mapToGlobal(QPoint(x, y) / scale_ratio),
  696. QSize(w, h) / scale_ratio);
  697. }
  698. void GMainWindow::SoftwareKeyboardHideInline() {
  699. if (!software_keyboard) {
  700. LOG_ERROR(Frontend, "The software keyboard is not initialized!");
  701. return;
  702. }
  703. software_keyboard->HideInlineKeyboard();
  704. }
  705. void GMainWindow::SoftwareKeyboardInlineTextChanged(
  706. Core::Frontend::InlineTextParameters text_parameters) {
  707. if (!software_keyboard) {
  708. LOG_ERROR(Frontend, "The software keyboard is not initialized!");
  709. return;
  710. }
  711. software_keyboard->InlineTextChanged(std::move(text_parameters));
  712. }
  713. void GMainWindow::SoftwareKeyboardExit() {
  714. if (!software_keyboard) {
  715. return;
  716. }
  717. software_keyboard->ExitKeyboard();
  718. software_keyboard = nullptr;
  719. }
  720. void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url,
  721. const std::string& additional_args, bool is_local) {
  722. #ifdef YUZU_USE_QT_WEB_ENGINE
  723. // Raw input breaks with the web applet, Disable web applets if enabled
  724. if (UISettings::values.disable_web_applet || Settings::values.enable_raw_input) {
  725. emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed,
  726. "http://localhost/");
  727. return;
  728. }
  729. web_applet = new QtNXWebEngineView(this, *system, input_subsystem.get());
  730. ui->action_Pause->setEnabled(false);
  731. ui->action_Restart->setEnabled(false);
  732. ui->action_Stop->setEnabled(false);
  733. {
  734. QProgressDialog loading_progress(this);
  735. loading_progress.setLabelText(tr("Loading Web Applet..."));
  736. loading_progress.setRange(0, 3);
  737. loading_progress.setValue(0);
  738. if (is_local && !Common::FS::Exists(main_url)) {
  739. loading_progress.show();
  740. auto future = QtConcurrent::run([this] { emit WebBrowserExtractOfflineRomFS(); });
  741. while (!future.isFinished()) {
  742. QCoreApplication::processEvents();
  743. std::this_thread::sleep_for(std::chrono::milliseconds(1));
  744. }
  745. }
  746. loading_progress.setValue(1);
  747. if (is_local) {
  748. web_applet->LoadLocalWebPage(main_url, additional_args);
  749. } else {
  750. web_applet->LoadExternalWebPage(main_url, additional_args);
  751. }
  752. if (render_window->IsLoadingComplete()) {
  753. render_window->hide();
  754. }
  755. const auto& layout = render_window->GetFramebufferLayout();
  756. const auto scale_ratio = devicePixelRatioF();
  757. web_applet->resize(layout.screen.GetWidth() / scale_ratio,
  758. layout.screen.GetHeight() / scale_ratio);
  759. web_applet->move(layout.screen.left / scale_ratio,
  760. (layout.screen.top / scale_ratio) + menuBar()->height());
  761. web_applet->setZoomFactor(static_cast<qreal>(layout.screen.GetWidth() / scale_ratio) /
  762. static_cast<qreal>(Layout::ScreenUndocked::Width));
  763. web_applet->setFocus();
  764. web_applet->show();
  765. loading_progress.setValue(2);
  766. QCoreApplication::processEvents();
  767. loading_progress.setValue(3);
  768. }
  769. bool exit_check = false;
  770. // TODO (Morph): Remove this
  771. QAction* exit_action = new QAction(tr("Disable Web Applet"), this);
  772. connect(exit_action, &QAction::triggered, this, [this] {
  773. const auto result = QMessageBox::warning(
  774. this, tr("Disable Web Applet"),
  775. tr("Disabling the web applet can lead to undefined behavior and should only be used "
  776. "with Super Mario 3D All-Stars. Are you sure you want to disable the web "
  777. "applet?\n(This can be re-enabled in the Debug settings.)"),
  778. QMessageBox::Yes | QMessageBox::No);
  779. if (result == QMessageBox::Yes) {
  780. UISettings::values.disable_web_applet = true;
  781. web_applet->SetFinished(true);
  782. }
  783. });
  784. ui->menubar->addAction(exit_action);
  785. while (!web_applet->IsFinished()) {
  786. QCoreApplication::processEvents();
  787. if (!exit_check) {
  788. web_applet->page()->runJavaScript(
  789. QStringLiteral("end_applet;"), [&](const QVariant& variant) {
  790. exit_check = false;
  791. if (variant.toBool()) {
  792. web_applet->SetFinished(true);
  793. web_applet->SetExitReason(
  794. Service::AM::Frontend::WebExitReason::EndButtonPressed);
  795. }
  796. });
  797. exit_check = true;
  798. }
  799. if (web_applet->GetCurrentURL().contains(QStringLiteral("localhost"))) {
  800. if (!web_applet->IsFinished()) {
  801. web_applet->SetFinished(true);
  802. web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::CallbackURL);
  803. }
  804. web_applet->SetLastURL(web_applet->GetCurrentURL().toStdString());
  805. }
  806. std::this_thread::sleep_for(std::chrono::milliseconds(1));
  807. }
  808. const auto exit_reason = web_applet->GetExitReason();
  809. const auto last_url = web_applet->GetLastURL();
  810. web_applet->hide();
  811. render_window->setFocus();
  812. if (render_window->IsLoadingComplete()) {
  813. render_window->show();
  814. }
  815. ui->action_Pause->setEnabled(true);
  816. ui->action_Restart->setEnabled(true);
  817. ui->action_Stop->setEnabled(true);
  818. ui->menubar->removeAction(exit_action);
  819. QCoreApplication::processEvents();
  820. emit WebBrowserClosed(exit_reason, last_url);
  821. #else
  822. // Utilize the same fallback as the default web browser applet.
  823. emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed, "http://localhost/");
  824. #endif
  825. }
  826. void GMainWindow::WebBrowserRequestExit() {
  827. #ifdef YUZU_USE_QT_WEB_ENGINE
  828. if (web_applet) {
  829. web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::ExitRequested);
  830. web_applet->SetFinished(true);
  831. }
  832. #endif
  833. }
  834. void GMainWindow::InitializeWidgets() {
  835. #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
  836. ui->action_Report_Compatibility->setVisible(true);
  837. #endif
  838. render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system);
  839. render_window->hide();
  840. game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this);
  841. ui->horizontalLayout->addWidget(game_list);
  842. game_list_placeholder = new GameListPlaceholder(this);
  843. ui->horizontalLayout->addWidget(game_list_placeholder);
  844. game_list_placeholder->setVisible(false);
  845. loading_screen = new LoadingScreen(this);
  846. loading_screen->hide();
  847. ui->horizontalLayout->addWidget(loading_screen);
  848. connect(loading_screen, &LoadingScreen::Hidden, [&] {
  849. loading_screen->Clear();
  850. if (emulation_running) {
  851. render_window->show();
  852. render_window->setFocus();
  853. }
  854. });
  855. multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room,
  856. ui->action_Show_Room, *system);
  857. multiplayer_state->setVisible(false);
  858. // Create status bar
  859. message_label = new QLabel();
  860. // Configured separately for left alignment
  861. message_label->setFrameStyle(QFrame::NoFrame);
  862. message_label->setContentsMargins(4, 0, 4, 0);
  863. message_label->setAlignment(Qt::AlignLeft);
  864. statusBar()->addPermanentWidget(message_label, 1);
  865. shader_building_label = new QLabel();
  866. shader_building_label->setToolTip(tr("The amount of shaders currently being built"));
  867. res_scale_label = new QLabel();
  868. res_scale_label->setToolTip(tr("The current selected resolution scaling multiplier."));
  869. emu_speed_label = new QLabel();
  870. emu_speed_label->setToolTip(
  871. tr("Current emulation speed. Values higher or lower than 100% "
  872. "indicate emulation is running faster or slower than a Switch."));
  873. game_fps_label = new QLabel();
  874. game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
  875. "This will vary from game to game and scene to scene."));
  876. emu_frametime_label = new QLabel();
  877. emu_frametime_label->setToolTip(
  878. tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For "
  879. "full-speed emulation this should be at most 16.67 ms."));
  880. for (auto& label : {shader_building_label, res_scale_label, emu_speed_label, game_fps_label,
  881. emu_frametime_label}) {
  882. label->setVisible(false);
  883. label->setFrameStyle(QFrame::NoFrame);
  884. label->setContentsMargins(4, 0, 4, 0);
  885. statusBar()->addPermanentWidget(label);
  886. }
  887. firmware_label = new QLabel();
  888. firmware_label->setObjectName(QStringLiteral("FirmwareLabel"));
  889. firmware_label->setVisible(false);
  890. firmware_label->setFocusPolicy(Qt::NoFocus);
  891. statusBar()->addPermanentWidget(firmware_label);
  892. statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
  893. statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
  894. tas_label = new QLabel();
  895. tas_label->setObjectName(QStringLiteral("TASlabel"));
  896. tas_label->setFocusPolicy(Qt::NoFocus);
  897. statusBar()->insertPermanentWidget(0, tas_label);
  898. volume_popup = new QWidget(this);
  899. volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup);
  900. volume_popup->setLayout(new QVBoxLayout());
  901. volume_popup->setMinimumWidth(200);
  902. volume_slider = new QSlider(Qt::Horizontal);
  903. volume_slider->setObjectName(QStringLiteral("volume_slider"));
  904. volume_slider->setMaximum(200);
  905. volume_slider->setPageStep(5);
  906. volume_popup->layout()->addWidget(volume_slider);
  907. volume_button = new VolumeButton();
  908. volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
  909. volume_button->setFocusPolicy(Qt::NoFocus);
  910. volume_button->setCheckable(true);
  911. UpdateVolumeUI();
  912. connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) {
  913. Settings::values.audio_muted = false;
  914. const auto volume = static_cast<u8>(percentage);
  915. Settings::values.volume.SetValue(volume);
  916. UpdateVolumeUI();
  917. });
  918. connect(volume_button, &QPushButton::clicked, this, [&] {
  919. UpdateVolumeUI();
  920. volume_popup->setVisible(!volume_popup->isVisible());
  921. QRect rect = volume_button->geometry();
  922. QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft());
  923. bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height());
  924. volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height())));
  925. });
  926. volume_button->setContextMenuPolicy(Qt::CustomContextMenu);
  927. connect(volume_button, &QPushButton::customContextMenuRequested,
  928. [this](const QPoint& menu_location) {
  929. QMenu context_menu;
  930. context_menu.addAction(
  931. Settings::values.audio_muted ? tr("Unmute") : tr("Mute"), [this] {
  932. Settings::values.audio_muted = !Settings::values.audio_muted;
  933. UpdateVolumeUI();
  934. });
  935. context_menu.addAction(tr("Reset Volume"), [this] {
  936. Settings::values.volume.SetValue(100);
  937. UpdateVolumeUI();
  938. });
  939. context_menu.exec(volume_button->mapToGlobal(menu_location));
  940. volume_button->repaint();
  941. });
  942. connect(volume_button, &VolumeButton::VolumeChanged, this, &GMainWindow::UpdateVolumeUI);
  943. statusBar()->insertPermanentWidget(0, volume_button);
  944. // setup AA button
  945. aa_status_button = new QPushButton();
  946. aa_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
  947. aa_status_button->setFocusPolicy(Qt::NoFocus);
  948. connect(aa_status_button, &QPushButton::clicked, [&] {
  949. auto aa_mode = Settings::values.anti_aliasing.GetValue();
  950. aa_mode = static_cast<Settings::AntiAliasing>(static_cast<u32>(aa_mode) + 1);
  951. if (aa_mode == Settings::AntiAliasing::MaxEnum) {
  952. aa_mode = Settings::AntiAliasing::None;
  953. }
  954. Settings::values.anti_aliasing.SetValue(aa_mode);
  955. aa_status_button->setChecked(true);
  956. UpdateAAText();
  957. });
  958. UpdateAAText();
  959. aa_status_button->setCheckable(true);
  960. aa_status_button->setChecked(true);
  961. aa_status_button->setContextMenuPolicy(Qt::CustomContextMenu);
  962. connect(aa_status_button, &QPushButton::customContextMenuRequested,
  963. [this](const QPoint& menu_location) {
  964. QMenu context_menu;
  965. for (auto const& aa_text_pair : ConfigurationShared::anti_aliasing_texts_map) {
  966. context_menu.addAction(aa_text_pair.second, [this, aa_text_pair] {
  967. Settings::values.anti_aliasing.SetValue(aa_text_pair.first);
  968. UpdateAAText();
  969. });
  970. }
  971. context_menu.exec(aa_status_button->mapToGlobal(menu_location));
  972. aa_status_button->repaint();
  973. });
  974. statusBar()->insertPermanentWidget(0, aa_status_button);
  975. // Setup Filter button
  976. filter_status_button = new QPushButton();
  977. filter_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
  978. filter_status_button->setFocusPolicy(Qt::NoFocus);
  979. connect(filter_status_button, &QPushButton::clicked, this,
  980. &GMainWindow::OnToggleAdaptingFilter);
  981. UpdateFilterText();
  982. filter_status_button->setCheckable(true);
  983. filter_status_button->setChecked(true);
  984. filter_status_button->setContextMenuPolicy(Qt::CustomContextMenu);
  985. connect(filter_status_button, &QPushButton::customContextMenuRequested,
  986. [this](const QPoint& menu_location) {
  987. QMenu context_menu;
  988. for (auto const& filter_text_pair : ConfigurationShared::scaling_filter_texts_map) {
  989. context_menu.addAction(filter_text_pair.second, [this, filter_text_pair] {
  990. Settings::values.scaling_filter.SetValue(filter_text_pair.first);
  991. UpdateFilterText();
  992. });
  993. }
  994. context_menu.exec(filter_status_button->mapToGlobal(menu_location));
  995. filter_status_button->repaint();
  996. });
  997. statusBar()->insertPermanentWidget(0, filter_status_button);
  998. // Setup Dock button
  999. dock_status_button = new QPushButton();
  1000. dock_status_button->setObjectName(QStringLiteral("DockingStatusBarButton"));
  1001. dock_status_button->setFocusPolicy(Qt::NoFocus);
  1002. connect(dock_status_button, &QPushButton::clicked, this, &GMainWindow::OnToggleDockedMode);
  1003. dock_status_button->setCheckable(true);
  1004. UpdateDockedButton();
  1005. dock_status_button->setContextMenuPolicy(Qt::CustomContextMenu);
  1006. connect(dock_status_button, &QPushButton::customContextMenuRequested,
  1007. [this](const QPoint& menu_location) {
  1008. QMenu context_menu;
  1009. for (auto const& pair : ConfigurationShared::use_docked_mode_texts_map) {
  1010. context_menu.addAction(pair.second, [this, &pair] {
  1011. if (pair.first != Settings::values.use_docked_mode.GetValue()) {
  1012. OnToggleDockedMode();
  1013. }
  1014. });
  1015. }
  1016. context_menu.exec(dock_status_button->mapToGlobal(menu_location));
  1017. dock_status_button->repaint();
  1018. });
  1019. statusBar()->insertPermanentWidget(0, dock_status_button);
  1020. // Setup GPU Accuracy button
  1021. gpu_accuracy_button = new QPushButton();
  1022. gpu_accuracy_button->setObjectName(QStringLiteral("GPUStatusBarButton"));
  1023. gpu_accuracy_button->setCheckable(true);
  1024. gpu_accuracy_button->setFocusPolicy(Qt::NoFocus);
  1025. connect(gpu_accuracy_button, &QPushButton::clicked, this, &GMainWindow::OnToggleGpuAccuracy);
  1026. UpdateGPUAccuracyButton();
  1027. gpu_accuracy_button->setContextMenuPolicy(Qt::CustomContextMenu);
  1028. connect(gpu_accuracy_button, &QPushButton::customContextMenuRequested,
  1029. [this](const QPoint& menu_location) {
  1030. QMenu context_menu;
  1031. for (auto const& gpu_accuracy_pair : ConfigurationShared::gpu_accuracy_texts_map) {
  1032. if (gpu_accuracy_pair.first == Settings::GpuAccuracy::Extreme) {
  1033. continue;
  1034. }
  1035. context_menu.addAction(gpu_accuracy_pair.second, [this, gpu_accuracy_pair] {
  1036. Settings::values.gpu_accuracy.SetValue(gpu_accuracy_pair.first);
  1037. UpdateGPUAccuracyButton();
  1038. });
  1039. }
  1040. context_menu.exec(gpu_accuracy_button->mapToGlobal(menu_location));
  1041. gpu_accuracy_button->repaint();
  1042. });
  1043. statusBar()->insertPermanentWidget(0, gpu_accuracy_button);
  1044. // Setup Renderer API button
  1045. renderer_status_button = new QPushButton();
  1046. renderer_status_button->setObjectName(QStringLiteral("RendererStatusBarButton"));
  1047. renderer_status_button->setCheckable(true);
  1048. renderer_status_button->setFocusPolicy(Qt::NoFocus);
  1049. connect(renderer_status_button, &QPushButton::clicked, this, &GMainWindow::OnToggleGraphicsAPI);
  1050. UpdateAPIText();
  1051. renderer_status_button->setCheckable(true);
  1052. renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() ==
  1053. Settings::RendererBackend::Vulkan);
  1054. renderer_status_button->setContextMenuPolicy(Qt::CustomContextMenu);
  1055. connect(renderer_status_button, &QPushButton::customContextMenuRequested,
  1056. [this](const QPoint& menu_location) {
  1057. QMenu context_menu;
  1058. for (auto const& renderer_backend_pair :
  1059. ConfigurationShared::renderer_backend_texts_map) {
  1060. if (renderer_backend_pair.first == Settings::RendererBackend::Null) {
  1061. continue;
  1062. }
  1063. context_menu.addAction(
  1064. renderer_backend_pair.second, [this, renderer_backend_pair] {
  1065. Settings::values.renderer_backend.SetValue(renderer_backend_pair.first);
  1066. UpdateAPIText();
  1067. });
  1068. }
  1069. context_menu.exec(renderer_status_button->mapToGlobal(menu_location));
  1070. renderer_status_button->repaint();
  1071. });
  1072. statusBar()->insertPermanentWidget(0, renderer_status_button);
  1073. statusBar()->setVisible(true);
  1074. setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}"));
  1075. }
  1076. void GMainWindow::InitializeDebugWidgets() {
  1077. QMenu* debug_menu = ui->menu_View_Debugging;
  1078. #if MICROPROFILE_ENABLED
  1079. microProfileDialog = new MicroProfileDialog(this);
  1080. microProfileDialog->hide();
  1081. debug_menu->addAction(microProfileDialog->toggleViewAction());
  1082. #endif
  1083. waitTreeWidget = new WaitTreeWidget(*system, this);
  1084. addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget);
  1085. waitTreeWidget->hide();
  1086. debug_menu->addAction(waitTreeWidget->toggleViewAction());
  1087. controller_dialog = new ControllerDialog(system->HIDCore(), input_subsystem, this);
  1088. controller_dialog->hide();
  1089. debug_menu->addAction(controller_dialog->toggleViewAction());
  1090. connect(this, &GMainWindow::EmulationStarting, waitTreeWidget,
  1091. &WaitTreeWidget::OnEmulationStarting);
  1092. connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
  1093. &WaitTreeWidget::OnEmulationStopping);
  1094. }
  1095. void GMainWindow::InitializeRecentFileMenuActions() {
  1096. for (int i = 0; i < max_recent_files_item; ++i) {
  1097. actions_recent_files[i] = new QAction(this);
  1098. actions_recent_files[i]->setVisible(false);
  1099. connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile);
  1100. ui->menu_recent_files->addAction(actions_recent_files[i]);
  1101. }
  1102. ui->menu_recent_files->addSeparator();
  1103. QAction* action_clear_recent_files = new QAction(this);
  1104. action_clear_recent_files->setText(tr("&Clear Recent Files"));
  1105. connect(action_clear_recent_files, &QAction::triggered, this, [this] {
  1106. UISettings::values.recent_files.clear();
  1107. UpdateRecentFiles();
  1108. });
  1109. ui->menu_recent_files->addAction(action_clear_recent_files);
  1110. UpdateRecentFiles();
  1111. }
  1112. void GMainWindow::LinkActionShortcut(QAction* action, const QString& action_name,
  1113. const bool tas_allowed) {
  1114. static const auto main_window = std::string("Main Window");
  1115. action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name.toStdString()));
  1116. action->setShortcutContext(
  1117. hotkey_registry.GetShortcutContext(main_window, action_name.toStdString()));
  1118. action->setAutoRepeat(false);
  1119. this->addAction(action);
  1120. auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
  1121. const auto* controller_hotkey =
  1122. hotkey_registry.GetControllerHotkey(main_window, action_name.toStdString(), controller);
  1123. connect(
  1124. controller_hotkey, &ControllerShortcut::Activated, this,
  1125. [action, tas_allowed, this] {
  1126. auto [tas_status, current_tas_frame, total_tas_frames] =
  1127. input_subsystem->GetTas()->GetStatus();
  1128. if (tas_allowed || tas_status == InputCommon::TasInput::TasState::Stopped) {
  1129. action->trigger();
  1130. }
  1131. },
  1132. Qt::QueuedConnection);
  1133. }
  1134. void GMainWindow::InitializeHotkeys() {
  1135. hotkey_registry.LoadHotkeys();
  1136. LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File"));
  1137. LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo"));
  1138. LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit yuzu"));
  1139. LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation"));
  1140. LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation"));
  1141. LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation"));
  1142. LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar"));
  1143. LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
  1144. LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen"));
  1145. LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
  1146. LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true);
  1147. LinkActionShortcut(ui->action_TAS_Record, QStringLiteral("TAS Record"), true);
  1148. LinkActionShortcut(ui->action_TAS_Reset, QStringLiteral("TAS Reset"), true);
  1149. LinkActionShortcut(ui->action_View_Lobby,
  1150. QStringLiteral("Multiplayer Browse Public Game Lobby"));
  1151. LinkActionShortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room"));
  1152. LinkActionShortcut(ui->action_Connect_To_Room,
  1153. QStringLiteral("Multiplayer Direct Connect to Room"));
  1154. LinkActionShortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room"));
  1155. LinkActionShortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room"));
  1156. static const QString main_window = QStringLiteral("Main Window");
  1157. const auto connect_shortcut = [&]<typename Fn>(const QString& action_name, const Fn& function) {
  1158. const auto* hotkey =
  1159. hotkey_registry.GetHotkey(main_window.toStdString(), action_name.toStdString(), this);
  1160. auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
  1161. const auto* controller_hotkey = hotkey_registry.GetControllerHotkey(
  1162. main_window.toStdString(), action_name.toStdString(), controller);
  1163. connect(hotkey, &QShortcut::activated, this, function);
  1164. connect(controller_hotkey, &ControllerShortcut::Activated, this, function,
  1165. Qt::QueuedConnection);
  1166. };
  1167. connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] {
  1168. if (emulation_running && ui->action_Fullscreen->isChecked()) {
  1169. ui->action_Fullscreen->setChecked(false);
  1170. ToggleFullscreen();
  1171. }
  1172. });
  1173. connect_shortcut(QStringLiteral("Change Adapting Filter"),
  1174. &GMainWindow::OnToggleAdaptingFilter);
  1175. connect_shortcut(QStringLiteral("Change Docked Mode"), &GMainWindow::OnToggleDockedMode);
  1176. connect_shortcut(QStringLiteral("Change GPU Accuracy"), &GMainWindow::OnToggleGpuAccuracy);
  1177. connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute);
  1178. connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume);
  1179. connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume);
  1180. connect_shortcut(QStringLiteral("Toggle Framerate Limit"), [] {
  1181. Settings::values.use_speed_limit.SetValue(!Settings::values.use_speed_limit.GetValue());
  1182. });
  1183. connect_shortcut(QStringLiteral("Toggle Renderdoc Capture"), [this] {
  1184. if (Settings::values.enable_renderdoc_hotkey) {
  1185. system->GetRenderdocAPI().ToggleCapture();
  1186. }
  1187. });
  1188. connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
  1189. Settings::values.mouse_panning = !Settings::values.mouse_panning;
  1190. if (Settings::values.mouse_panning) {
  1191. render_window->installEventFilter(render_window);
  1192. render_window->setAttribute(Qt::WA_Hover, true);
  1193. }
  1194. });
  1195. }
  1196. void GMainWindow::SetDefaultUIGeometry() {
  1197. // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half
  1198. const QRect screenRect = QGuiApplication::primaryScreen()->geometry();
  1199. const int w = screenRect.width() * 2 / 3;
  1200. const int h = screenRect.height() * 2 / 3;
  1201. const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2;
  1202. const int y = (screenRect.y() + screenRect.height()) / 2 - h * 53 / 100;
  1203. setGeometry(x, y, w, h);
  1204. }
  1205. void GMainWindow::RestoreUIState() {
  1206. setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
  1207. restoreGeometry(UISettings::values.geometry);
  1208. // Work-around because the games list isn't supposed to be full screen
  1209. if (isFullScreen()) {
  1210. showNormal();
  1211. }
  1212. restoreState(UISettings::values.state);
  1213. render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint);
  1214. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  1215. #if MICROPROFILE_ENABLED
  1216. microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
  1217. microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue());
  1218. #endif
  1219. game_list->LoadInterfaceLayout();
  1220. ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue());
  1221. ToggleWindowMode();
  1222. ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue());
  1223. ui->action_Display_Dock_Widget_Headers->setChecked(
  1224. UISettings::values.display_titlebar.GetValue());
  1225. OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked());
  1226. ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue());
  1227. game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
  1228. ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
  1229. statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
  1230. Debugger::ToggleConsole();
  1231. }
  1232. void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
  1233. if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive &&
  1234. state != Qt::ApplicationActive) {
  1235. LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state);
  1236. }
  1237. if (!emulation_running) {
  1238. return;
  1239. }
  1240. if (UISettings::values.pause_when_in_background) {
  1241. if (emu_thread->IsRunning() &&
  1242. (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) {
  1243. auto_paused = true;
  1244. OnPauseGame();
  1245. } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) {
  1246. auto_paused = false;
  1247. RequestGameResume();
  1248. OnStartGame();
  1249. }
  1250. }
  1251. if (UISettings::values.mute_when_in_background) {
  1252. if (!Settings::values.audio_muted &&
  1253. (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) {
  1254. Settings::values.audio_muted = true;
  1255. auto_muted = true;
  1256. } else if (auto_muted && state == Qt::ApplicationActive) {
  1257. Settings::values.audio_muted = false;
  1258. auto_muted = false;
  1259. }
  1260. UpdateVolumeUI();
  1261. }
  1262. }
  1263. void GMainWindow::ConnectWidgetEvents() {
  1264. connect(game_list, &GameList::BootGame, this, &GMainWindow::BootGameFromList);
  1265. connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
  1266. connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
  1267. connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
  1268. connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,
  1269. &GMainWindow::OnTransferableShaderCacheOpenFile);
  1270. connect(game_list, &GameList::RemoveInstalledEntryRequested, this,
  1271. &GMainWindow::OnGameListRemoveInstalledEntry);
  1272. connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
  1273. connect(game_list, &GameList::RemovePlayTimeRequested, this,
  1274. &GMainWindow::OnGameListRemovePlayTimeData);
  1275. connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
  1276. connect(game_list, &GameList::VerifyIntegrityRequested, this,
  1277. &GMainWindow::OnGameListVerifyIntegrity);
  1278. connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
  1279. connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
  1280. &GMainWindow::OnGameListNavigateToGamedbEntry);
  1281. connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);
  1282. connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
  1283. connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
  1284. &GMainWindow::OnGameListAddDirectory);
  1285. connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
  1286. connect(game_list, &GameList::PopulatingCompleted,
  1287. [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); });
  1288. connect(game_list, &GameList::SaveConfig, this, &GMainWindow::OnSaveConfig);
  1289. connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
  1290. &GMainWindow::OnGameListOpenPerGameProperties);
  1291. connect(this, &GMainWindow::UpdateInstallProgress, this,
  1292. &GMainWindow::IncrementInstallProgress);
  1293. connect(this, &GMainWindow::EmulationStarting, render_window,
  1294. &GRenderWindow::OnEmulationStarting);
  1295. connect(this, &GMainWindow::EmulationStopping, render_window,
  1296. &GRenderWindow::OnEmulationStopping);
  1297. // Software Keyboard Applet
  1298. connect(this, &GMainWindow::EmulationStarting, this, &GMainWindow::SoftwareKeyboardExit);
  1299. connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit);
  1300. connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
  1301. connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state,
  1302. &MultiplayerState::UpdateThemedIcons);
  1303. }
  1304. void GMainWindow::ConnectMenuEvents() {
  1305. const auto connect_menu = [&]<typename Fn>(QAction* action, const Fn& event_fn) {
  1306. connect(action, &QAction::triggered, this, event_fn);
  1307. // Add actions to this window so that hiding menus in fullscreen won't disable them
  1308. addAction(action);
  1309. // Add actions to the render window so that they work outside of single window mode
  1310. render_window->addAction(action);
  1311. };
  1312. // File
  1313. connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile);
  1314. connect_menu(ui->action_Load_Folder, &GMainWindow::OnMenuLoadFolder);
  1315. connect_menu(ui->action_Install_File_NAND, &GMainWindow::OnMenuInstallToNAND);
  1316. connect_menu(ui->action_Exit, &QMainWindow::close);
  1317. connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo);
  1318. // Emulation
  1319. connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame);
  1320. connect_menu(ui->action_Stop, &GMainWindow::OnStopGame);
  1321. connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility);
  1322. connect_menu(ui->action_Open_Mods_Page, &GMainWindow::OnOpenModsPage);
  1323. connect_menu(ui->action_Open_Quickstart_Guide, &GMainWindow::OnOpenQuickstartGuide);
  1324. connect_menu(ui->action_Open_FAQ, &GMainWindow::OnOpenFAQ);
  1325. connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame);
  1326. connect_menu(ui->action_Configure, &GMainWindow::OnConfigure);
  1327. connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame);
  1328. // View
  1329. connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen);
  1330. connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode);
  1331. connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars);
  1332. connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar);
  1333. connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar);
  1334. connect_menu(ui->action_Reset_Window_Size_720, &GMainWindow::ResetWindowSize720);
  1335. connect_menu(ui->action_Reset_Window_Size_900, &GMainWindow::ResetWindowSize900);
  1336. connect_menu(ui->action_Reset_Window_Size_1080, &GMainWindow::ResetWindowSize1080);
  1337. ui->menu_Reset_Window_Size->addActions({ui->action_Reset_Window_Size_720,
  1338. ui->action_Reset_Window_Size_900,
  1339. ui->action_Reset_Window_Size_1080});
  1340. // Multiplayer
  1341. connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state,
  1342. &MultiplayerState::OnViewLobby);
  1343. connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state,
  1344. &MultiplayerState::OnCreateRoom);
  1345. connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state,
  1346. &MultiplayerState::OnCloseRoom);
  1347. connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state,
  1348. &MultiplayerState::OnDirectConnectToRoom);
  1349. connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state,
  1350. &MultiplayerState::OnOpenNetworkRoom);
  1351. connect(multiplayer_state, &MultiplayerState::SaveConfig, this, &GMainWindow::OnSaveConfig);
  1352. // Tools
  1353. connect_menu(ui->action_Load_Album, &GMainWindow::OnAlbum);
  1354. connect_menu(ui->action_Load_Cabinet_Nickname_Owner,
  1355. [this]() { OnCabinet(Service::NFP::CabinetMode::StartNicknameAndOwnerSettings); });
  1356. connect_menu(ui->action_Load_Cabinet_Eraser,
  1357. [this]() { OnCabinet(Service::NFP::CabinetMode::StartGameDataEraser); });
  1358. connect_menu(ui->action_Load_Cabinet_Restorer,
  1359. [this]() { OnCabinet(Service::NFP::CabinetMode::StartRestorer); });
  1360. connect_menu(ui->action_Load_Cabinet_Formatter,
  1361. [this]() { OnCabinet(Service::NFP::CabinetMode::StartFormatter); });
  1362. connect_menu(ui->action_Load_Mii_Edit, &GMainWindow::OnMiiEdit);
  1363. connect_menu(ui->action_Open_Controller_Menu, &GMainWindow::OnOpenControllerMenu);
  1364. connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
  1365. // TAS
  1366. connect_menu(ui->action_TAS_Start, &GMainWindow::OnTasStartStop);
  1367. connect_menu(ui->action_TAS_Record, &GMainWindow::OnTasRecord);
  1368. connect_menu(ui->action_TAS_Reset, &GMainWindow::OnTasReset);
  1369. connect_menu(ui->action_Configure_Tas, &GMainWindow::OnConfigureTas);
  1370. // Help
  1371. connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder);
  1372. connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
  1373. connect_menu(ui->action_About, &GMainWindow::OnAbout);
  1374. }
  1375. void GMainWindow::UpdateMenuState() {
  1376. const bool is_paused = emu_thread == nullptr || !emu_thread->IsRunning();
  1377. const bool is_firmware_available = CheckFirmwarePresence();
  1378. const std::array running_actions{
  1379. ui->action_Stop,
  1380. ui->action_Restart,
  1381. ui->action_Configure_Current_Game,
  1382. ui->action_Report_Compatibility,
  1383. ui->action_Load_Amiibo,
  1384. ui->action_Pause,
  1385. };
  1386. const std::array applet_actions{ui->action_Load_Album,
  1387. ui->action_Load_Cabinet_Nickname_Owner,
  1388. ui->action_Load_Cabinet_Eraser,
  1389. ui->action_Load_Cabinet_Restorer,
  1390. ui->action_Load_Cabinet_Formatter,
  1391. ui->action_Load_Mii_Edit,
  1392. ui->action_Open_Controller_Menu};
  1393. for (QAction* action : running_actions) {
  1394. action->setEnabled(emulation_running);
  1395. }
  1396. for (QAction* action : applet_actions) {
  1397. action->setEnabled(is_firmware_available && !emulation_running);
  1398. }
  1399. ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused);
  1400. if (emulation_running && is_paused) {
  1401. ui->action_Pause->setText(tr("&Continue"));
  1402. } else {
  1403. ui->action_Pause->setText(tr("&Pause"));
  1404. }
  1405. multiplayer_state->UpdateNotificationStatus();
  1406. }
  1407. void GMainWindow::OnDisplayTitleBars(bool show) {
  1408. QList<QDockWidget*> widgets = findChildren<QDockWidget*>();
  1409. if (show) {
  1410. for (QDockWidget* widget : widgets) {
  1411. QWidget* old = widget->titleBarWidget();
  1412. widget->setTitleBarWidget(nullptr);
  1413. if (old != nullptr)
  1414. delete old;
  1415. }
  1416. } else {
  1417. for (QDockWidget* widget : widgets) {
  1418. QWidget* old = widget->titleBarWidget();
  1419. widget->setTitleBarWidget(new QWidget());
  1420. if (old != nullptr)
  1421. delete old;
  1422. }
  1423. }
  1424. }
  1425. void GMainWindow::SetupPrepareForSleep() {
  1426. #ifdef __unix__
  1427. auto bus = QDBusConnection::systemBus();
  1428. if (bus.isConnected()) {
  1429. const bool success = bus.connect(
  1430. QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"),
  1431. QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("PrepareForSleep"),
  1432. QStringLiteral("b"), this, SLOT(OnPrepareForSleep(bool)));
  1433. if (!success) {
  1434. LOG_WARNING(Frontend, "Couldn't register PrepareForSleep signal");
  1435. }
  1436. } else {
  1437. LOG_WARNING(Frontend, "QDBusConnection system bus is not connected");
  1438. }
  1439. #endif // __unix__
  1440. }
  1441. void GMainWindow::OnPrepareForSleep(bool prepare_sleep) {
  1442. if (emu_thread == nullptr) {
  1443. return;
  1444. }
  1445. if (prepare_sleep) {
  1446. if (emu_thread->IsRunning()) {
  1447. auto_paused = true;
  1448. OnPauseGame();
  1449. }
  1450. } else {
  1451. if (!emu_thread->IsRunning() && auto_paused) {
  1452. auto_paused = false;
  1453. RequestGameResume();
  1454. OnStartGame();
  1455. }
  1456. }
  1457. }
  1458. #ifdef __unix__
  1459. std::array<int, 3> GMainWindow::sig_interrupt_fds{0, 0, 0};
  1460. void GMainWindow::SetupSigInterrupts() {
  1461. if (sig_interrupt_fds[2] == 1) {
  1462. return;
  1463. }
  1464. socketpair(AF_UNIX, SOCK_STREAM, 0, sig_interrupt_fds.data());
  1465. sig_interrupt_fds[2] = 1;
  1466. struct sigaction sa;
  1467. sa.sa_handler = &GMainWindow::HandleSigInterrupt;
  1468. sigemptyset(&sa.sa_mask);
  1469. sa.sa_flags = SA_RESETHAND;
  1470. sigaction(SIGINT, &sa, nullptr);
  1471. sigaction(SIGTERM, &sa, nullptr);
  1472. sig_interrupt_notifier = new QSocketNotifier(sig_interrupt_fds[1], QSocketNotifier::Read, this);
  1473. connect(sig_interrupt_notifier, &QSocketNotifier::activated, this,
  1474. &GMainWindow::OnSigInterruptNotifierActivated);
  1475. connect(this, &GMainWindow::SigInterrupt, this, &GMainWindow::close);
  1476. }
  1477. void GMainWindow::HandleSigInterrupt(int sig) {
  1478. if (sig == SIGINT) {
  1479. _exit(1);
  1480. }
  1481. // Calling into Qt directly from a signal handler is not safe,
  1482. // so wake up a QSocketNotifier with this hacky write call instead.
  1483. char a = 1;
  1484. int ret = write(sig_interrupt_fds[0], &a, sizeof(a));
  1485. (void)ret;
  1486. }
  1487. void GMainWindow::OnSigInterruptNotifierActivated() {
  1488. sig_interrupt_notifier->setEnabled(false);
  1489. char a;
  1490. int ret = read(sig_interrupt_fds[1], &a, sizeof(a));
  1491. (void)ret;
  1492. sig_interrupt_notifier->setEnabled(true);
  1493. emit SigInterrupt();
  1494. }
  1495. #endif // __unix__
  1496. void GMainWindow::PreventOSSleep() {
  1497. #ifdef _WIN32
  1498. SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
  1499. #elif defined(HAVE_SDL2)
  1500. SDL_DisableScreenSaver();
  1501. #endif
  1502. }
  1503. void GMainWindow::AllowOSSleep() {
  1504. #ifdef _WIN32
  1505. SetThreadExecutionState(ES_CONTINUOUS);
  1506. #elif defined(HAVE_SDL2)
  1507. SDL_EnableScreenSaver();
  1508. #endif
  1509. }
  1510. bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params) {
  1511. // Shutdown previous session if the emu thread is still active...
  1512. if (emu_thread != nullptr) {
  1513. ShutdownGame();
  1514. }
  1515. if (!render_window->InitRenderTarget()) {
  1516. return false;
  1517. }
  1518. system->SetFilesystem(vfs);
  1519. if (params.launch_type == Service::AM::LaunchType::FrontendInitiated) {
  1520. system->GetUserChannel().clear();
  1521. }
  1522. system->SetFrontendAppletSet({
  1523. std::make_unique<QtAmiiboSettings>(*this), // Amiibo Settings
  1524. (UISettings::values.controller_applet_disabled.GetValue() == true)
  1525. ? nullptr
  1526. : std::make_unique<QtControllerSelector>(*this), // Controller Selector
  1527. std::make_unique<QtErrorDisplay>(*this), // Error Display
  1528. nullptr, // Mii Editor
  1529. nullptr, // Parental Controls
  1530. nullptr, // Photo Viewer
  1531. std::make_unique<QtProfileSelector>(*this), // Profile Selector
  1532. std::make_unique<QtSoftwareKeyboard>(*this), // Software Keyboard
  1533. std::make_unique<QtWebBrowser>(*this), // Web Browser
  1534. });
  1535. const Core::SystemResultStatus result{
  1536. system->Load(*render_window, filename.toStdString(), params)};
  1537. const auto drd_callout = (UISettings::values.callout_flags.GetValue() &
  1538. static_cast<u32>(CalloutFlag::DRDDeprecation)) == 0;
  1539. if (result == Core::SystemResultStatus::Success &&
  1540. system->GetAppLoader().GetFileType() == Loader::FileType::DeconstructedRomDirectory &&
  1541. drd_callout) {
  1542. UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() |
  1543. static_cast<u32>(CalloutFlag::DRDDeprecation);
  1544. QMessageBox::warning(
  1545. this, tr("Warning Outdated Game Format"),
  1546. tr("You are using the deconstructed ROM directory format for this game, which is an "
  1547. "outdated format that has been superseded by others such as NCA, NAX, XCI, or "
  1548. "NSP. Deconstructed ROM directories lack icons, metadata, and update "
  1549. "support.<br><br>For an explanation of the various Switch formats yuzu supports, <a "
  1550. "href='https://yuzu-emu.org/wiki/overview-of-switch-game-formats'>check out our "
  1551. "wiki</a>. This message will not be shown again."));
  1552. }
  1553. if (result != Core::SystemResultStatus::Success) {
  1554. switch (result) {
  1555. case Core::SystemResultStatus::ErrorGetLoader:
  1556. LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString());
  1557. QMessageBox::critical(this, tr("Error while loading ROM!"),
  1558. tr("The ROM format is not supported."));
  1559. break;
  1560. case Core::SystemResultStatus::ErrorVideoCore:
  1561. QMessageBox::critical(
  1562. this, tr("An error occurred initializing the video core."),
  1563. tr("yuzu has encountered an error while running the video core. "
  1564. "This is usually caused by outdated GPU drivers, including integrated ones. "
  1565. "Please see the log for more details. "
  1566. "For more information on accessing the log, please see the following page: "
  1567. "<a href='https://yuzu-emu.org/help/reference/log-files/'>"
  1568. "How to Upload the Log File</a>. "));
  1569. break;
  1570. default:
  1571. if (result > Core::SystemResultStatus::ErrorLoader) {
  1572. const u16 loader_id = static_cast<u16>(Core::SystemResultStatus::ErrorLoader);
  1573. const u16 error_id = static_cast<u16>(result) - loader_id;
  1574. const std::string error_code = fmt::format("({:04X}-{:04X})", loader_id, error_id);
  1575. LOG_CRITICAL(Frontend, "Failed to load ROM! {}", error_code);
  1576. const auto title =
  1577. tr("Error while loading ROM! %1", "%1 signifies a numeric error code.")
  1578. .arg(QString::fromStdString(error_code));
  1579. const auto description =
  1580. tr("%1<br>Please follow <a href='https://yuzu-emu.org/help/quickstart/'>the "
  1581. "yuzu quickstart guide</a> to redump your files.<br>You can refer "
  1582. "to the yuzu wiki</a> or the yuzu Discord</a> for help.",
  1583. "%1 signifies an error string.")
  1584. .arg(QString::fromStdString(
  1585. GetResultStatusString(static_cast<Loader::ResultStatus>(error_id))));
  1586. QMessageBox::critical(this, title, description);
  1587. } else {
  1588. QMessageBox::critical(
  1589. this, tr("Error while loading ROM!"),
  1590. tr("An unknown error occurred. Please see the log for more details."));
  1591. }
  1592. break;
  1593. }
  1594. return false;
  1595. }
  1596. current_game_path = filename;
  1597. system->TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
  1598. return true;
  1599. }
  1600. bool GMainWindow::SelectAndSetCurrentUser(
  1601. const Core::Frontend::ProfileSelectParameters& parameters) {
  1602. QtProfileSelectionDialog dialog(*system, this, parameters);
  1603. dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
  1604. Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
  1605. dialog.setWindowModality(Qt::WindowModal);
  1606. if (dialog.exec() == QDialog::Rejected) {
  1607. return false;
  1608. }
  1609. Settings::values.current_user = dialog.GetIndex();
  1610. return true;
  1611. }
  1612. void GMainWindow::ConfigureFilesystemProvider(const std::string& filepath) {
  1613. // Ensure all NCAs are registered before launching the game
  1614. const auto file = vfs->OpenFile(filepath, FileSys::OpenMode::Read);
  1615. if (!file) {
  1616. return;
  1617. }
  1618. auto loader = Loader::GetLoader(*system, file);
  1619. if (!loader) {
  1620. return;
  1621. }
  1622. const auto file_type = loader->GetFileType();
  1623. if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
  1624. return;
  1625. }
  1626. u64 program_id = 0;
  1627. const auto res2 = loader->ReadProgramId(program_id);
  1628. if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
  1629. provider->AddEntry(FileSys::TitleType::Application,
  1630. FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id,
  1631. file);
  1632. } else if (res2 == Loader::ResultStatus::Success &&
  1633. (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
  1634. const auto nsp = file_type == Loader::FileType::NSP
  1635. ? std::make_shared<FileSys::NSP>(file)
  1636. : FileSys::XCI{file}.GetSecurePartitionNSP();
  1637. for (const auto& title : nsp->GetNCAs()) {
  1638. for (const auto& entry : title.second) {
  1639. provider->AddEntry(entry.first.first, entry.first.second, title.first,
  1640. entry.second->GetBaseFile());
  1641. }
  1642. }
  1643. }
  1644. }
  1645. void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletParameters params,
  1646. StartGameType type) {
  1647. LOG_INFO(Frontend, "yuzu starting...");
  1648. if (params.program_id == 0 ||
  1649. params.program_id > static_cast<u64>(Service::AM::AppletProgramId::MaxProgramId)) {
  1650. StoreRecentFile(filename); // Put the filename on top of the list
  1651. }
  1652. // Save configurations
  1653. UpdateUISettings();
  1654. game_list->SaveInterfaceLayout();
  1655. config->SaveAllValues();
  1656. u64 title_id{0};
  1657. last_filename_booted = filename;
  1658. ConfigureFilesystemProvider(filename.toStdString());
  1659. const auto v_file = Core::GetGameFileFromPath(vfs, filename.toUtf8().constData());
  1660. const auto loader = Loader::GetLoader(*system, v_file, params.program_id, params.program_index);
  1661. if (loader != nullptr && loader->ReadProgramId(title_id) == Loader::ResultStatus::Success &&
  1662. type == StartGameType::Normal) {
  1663. // Load per game settings
  1664. const auto file_path =
  1665. std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())};
  1666. const auto config_file_name = title_id == 0
  1667. ? Common::FS::PathToUTF8String(file_path.filename())
  1668. : fmt::format("{:016X}", title_id);
  1669. QtConfig per_game_config(config_file_name, Config::ConfigType::PerGameConfig);
  1670. system->HIDCore().ReloadInputDevices();
  1671. system->ApplySettings();
  1672. }
  1673. Settings::LogSettings();
  1674. if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) {
  1675. const Core::Frontend::ProfileSelectParameters parameters{
  1676. .mode = Service::AM::Frontend::UiMode::UserSelector,
  1677. .invalid_uid_list = {},
  1678. .display_options = {},
  1679. .purpose = Service::AM::Frontend::UserSelectionPurpose::General,
  1680. };
  1681. if (SelectAndSetCurrentUser(parameters) == false) {
  1682. return;
  1683. }
  1684. }
  1685. // If the user specifies -u (successfully) on the cmd line, don't prompt for a user on first
  1686. // game startup only. If the user stops emulation and starts a new one, go back to the expected
  1687. // behavior of asking.
  1688. user_flag_cmd_line = false;
  1689. if (!LoadROM(filename, params)) {
  1690. return;
  1691. }
  1692. system->SetShuttingDown(false);
  1693. game_list->setDisabled(true);
  1694. // Create and start the emulation thread
  1695. emu_thread = std::make_unique<EmuThread>(*system);
  1696. emit EmulationStarting(emu_thread.get());
  1697. emu_thread->start();
  1698. // Register an ExecuteProgram callback such that Core can execute a sub-program
  1699. system->RegisterExecuteProgramCallback(
  1700. [this](std::size_t program_index_) { render_window->ExecuteProgram(program_index_); });
  1701. system->RegisterExitCallback([this] {
  1702. emu_thread->ForceStop();
  1703. render_window->Exit();
  1704. });
  1705. connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  1706. connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity);
  1707. // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views
  1708. // before the CPU continues
  1709. connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget,
  1710. &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection);
  1711. connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget,
  1712. &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection);
  1713. connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen,
  1714. &LoadingScreen::OnLoadProgress, Qt::QueuedConnection);
  1715. // Update the GUI
  1716. UpdateStatusButtons();
  1717. if (ui->action_Single_Window_Mode->isChecked()) {
  1718. game_list->hide();
  1719. game_list_placeholder->hide();
  1720. }
  1721. status_bar_update_timer.start(500);
  1722. renderer_status_button->setDisabled(true);
  1723. if (UISettings::values.hide_mouse || Settings::values.mouse_panning) {
  1724. render_window->installEventFilter(render_window);
  1725. render_window->setAttribute(Qt::WA_Hover, true);
  1726. }
  1727. if (UISettings::values.hide_mouse) {
  1728. mouse_hide_timer.start();
  1729. }
  1730. render_window->InitializeCamera();
  1731. std::string title_name;
  1732. std::string title_version;
  1733. const auto res = system->GetGameName(title_name);
  1734. const auto metadata = [this, title_id] {
  1735. const FileSys::PatchManager pm(title_id, system->GetFileSystemController(),
  1736. system->GetContentProvider());
  1737. return pm.GetControlMetadata();
  1738. }();
  1739. if (metadata.first != nullptr) {
  1740. title_version = metadata.first->GetVersionString();
  1741. title_name = metadata.first->GetApplicationName();
  1742. }
  1743. if (res != Loader::ResultStatus::Success || title_name.empty()) {
  1744. title_name = Common::FS::PathToUTF8String(
  1745. std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())}
  1746. .filename());
  1747. }
  1748. const bool is_64bit = system->Kernel().ApplicationProcess()->Is64Bit();
  1749. const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)");
  1750. title_name = tr("%1 %2", "%1 is the title name. %2 indicates if the title is 64-bit or 32-bit")
  1751. .arg(QString::fromStdString(title_name), instruction_set_suffix)
  1752. .toStdString();
  1753. LOG_INFO(Frontend, "Booting game: {:016X} | {} | {}", title_id, title_name, title_version);
  1754. const auto gpu_vendor = system->GPU().Renderer().GetDeviceVendor();
  1755. UpdateWindowTitle(title_name, title_version, gpu_vendor);
  1756. loading_screen->Prepare(system->GetAppLoader());
  1757. loading_screen->show();
  1758. emulation_running = true;
  1759. if (ui->action_Fullscreen->isChecked()) {
  1760. ShowFullscreen();
  1761. }
  1762. OnStartGame();
  1763. }
  1764. void GMainWindow::BootGameFromList(const QString& filename, StartGameType with_config) {
  1765. BootGame(filename, ApplicationAppletParameters(), with_config);
  1766. }
  1767. bool GMainWindow::OnShutdownBegin() {
  1768. if (!emulation_running) {
  1769. return false;
  1770. }
  1771. if (ui->action_Fullscreen->isChecked()) {
  1772. HideFullscreen();
  1773. }
  1774. AllowOSSleep();
  1775. // Disable unlimited frame rate
  1776. Settings::values.use_speed_limit.SetValue(true);
  1777. if (system->IsShuttingDown()) {
  1778. return false;
  1779. }
  1780. system->SetShuttingDown(true);
  1781. discord_rpc->Pause();
  1782. RequestGameExit();
  1783. emu_thread->disconnect();
  1784. emu_thread->SetRunning(true);
  1785. emit EmulationStopping();
  1786. int shutdown_time = 1000;
  1787. if (system->DebuggerEnabled()) {
  1788. shutdown_time = 0;
  1789. } else if (system->GetExitLocked()) {
  1790. shutdown_time = 5000;
  1791. }
  1792. shutdown_timer.setSingleShot(true);
  1793. shutdown_timer.start(shutdown_time);
  1794. connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired);
  1795. connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped);
  1796. // Disable everything to prevent anything from being triggered here
  1797. ui->action_Pause->setEnabled(false);
  1798. ui->action_Restart->setEnabled(false);
  1799. ui->action_Stop->setEnabled(false);
  1800. return true;
  1801. }
  1802. void GMainWindow::OnShutdownBeginDialog() {
  1803. shutdown_dialog = new OverlayDialog(this, *system, QString{}, tr("Closing software..."),
  1804. QString{}, QString{}, Qt::AlignHCenter | Qt::AlignVCenter);
  1805. shutdown_dialog->open();
  1806. }
  1807. void GMainWindow::OnEmulationStopTimeExpired() {
  1808. if (emu_thread) {
  1809. emu_thread->ForceStop();
  1810. }
  1811. }
  1812. void GMainWindow::OnEmulationStopped() {
  1813. shutdown_timer.stop();
  1814. if (emu_thread) {
  1815. emu_thread->disconnect();
  1816. emu_thread->wait();
  1817. emu_thread.reset();
  1818. }
  1819. if (shutdown_dialog) {
  1820. shutdown_dialog->deleteLater();
  1821. shutdown_dialog = nullptr;
  1822. }
  1823. emulation_running = false;
  1824. discord_rpc->Update();
  1825. #ifdef __unix__
  1826. Common::Linux::StopGamemode();
  1827. #endif
  1828. // The emulation is stopped, so closing the window or not does not matter anymore
  1829. disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
  1830. // Update the GUI
  1831. UpdateMenuState();
  1832. render_window->hide();
  1833. loading_screen->hide();
  1834. loading_screen->Clear();
  1835. if (game_list->IsEmpty()) {
  1836. game_list_placeholder->show();
  1837. } else {
  1838. game_list->show();
  1839. }
  1840. game_list->SetFilterFocus();
  1841. tas_label->clear();
  1842. input_subsystem->GetTas()->Stop();
  1843. OnTasStateChanged();
  1844. render_window->FinalizeCamera();
  1845. system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::None);
  1846. // Enable all controllers
  1847. system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
  1848. render_window->removeEventFilter(render_window);
  1849. render_window->setAttribute(Qt::WA_Hover, false);
  1850. UpdateWindowTitle();
  1851. // Disable status bar updates
  1852. status_bar_update_timer.stop();
  1853. shader_building_label->setVisible(false);
  1854. res_scale_label->setVisible(false);
  1855. emu_speed_label->setVisible(false);
  1856. game_fps_label->setVisible(false);
  1857. emu_frametime_label->setVisible(false);
  1858. renderer_status_button->setEnabled(!UISettings::values.has_broken_vulkan);
  1859. if (!firmware_label->text().isEmpty()) {
  1860. firmware_label->setVisible(true);
  1861. }
  1862. current_game_path.clear();
  1863. // When closing the game, destroy the GLWindow to clear the context after the game is closed
  1864. render_window->ReleaseRenderTarget();
  1865. // Enable game list
  1866. game_list->setEnabled(true);
  1867. Settings::RestoreGlobalState(system->IsPoweredOn());
  1868. system->HIDCore().ReloadInputDevices();
  1869. UpdateStatusButtons();
  1870. }
  1871. void GMainWindow::ShutdownGame() {
  1872. if (!emulation_running) {
  1873. return;
  1874. }
  1875. play_time_manager->Stop();
  1876. OnShutdownBegin();
  1877. OnEmulationStopTimeExpired();
  1878. OnEmulationStopped();
  1879. }
  1880. void GMainWindow::StoreRecentFile(const QString& filename) {
  1881. UISettings::values.recent_files.prepend(filename);
  1882. UISettings::values.recent_files.removeDuplicates();
  1883. while (UISettings::values.recent_files.size() > max_recent_files_item) {
  1884. UISettings::values.recent_files.removeLast();
  1885. }
  1886. UpdateRecentFiles();
  1887. }
  1888. void GMainWindow::UpdateRecentFiles() {
  1889. const int num_recent_files =
  1890. std::min(static_cast<int>(UISettings::values.recent_files.size()), max_recent_files_item);
  1891. for (int i = 0; i < num_recent_files; i++) {
  1892. const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg(
  1893. QFileInfo(UISettings::values.recent_files[i]).fileName());
  1894. actions_recent_files[i]->setText(text);
  1895. actions_recent_files[i]->setData(UISettings::values.recent_files[i]);
  1896. actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]);
  1897. actions_recent_files[i]->setVisible(true);
  1898. }
  1899. for (int j = num_recent_files; j < max_recent_files_item; ++j) {
  1900. actions_recent_files[j]->setVisible(false);
  1901. }
  1902. // Enable the recent files menu if the list isn't empty
  1903. ui->menu_recent_files->setEnabled(num_recent_files != 0);
  1904. }
  1905. void GMainWindow::OnGameListLoadFile(QString game_path, u64 program_id) {
  1906. auto params = ApplicationAppletParameters();
  1907. params.program_id = program_id;
  1908. BootGame(game_path, params);
  1909. }
  1910. void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target,
  1911. const std::string& game_path) {
  1912. std::filesystem::path path;
  1913. QString open_target;
  1914. const auto [user_save_size, device_save_size] = [this, &game_path, &program_id] {
  1915. const FileSys::PatchManager pm{program_id, system->GetFileSystemController(),
  1916. system->GetContentProvider()};
  1917. const auto control = pm.GetControlMetadata().first;
  1918. if (control != nullptr) {
  1919. return std::make_pair(control->GetDefaultNormalSaveSize(),
  1920. control->GetDeviceSaveDataSize());
  1921. } else {
  1922. const auto file = Core::GetGameFileFromPath(vfs, game_path);
  1923. const auto loader = Loader::GetLoader(*system, file);
  1924. FileSys::NACP nacp{};
  1925. loader->ReadControlData(nacp);
  1926. return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize());
  1927. }
  1928. }();
  1929. const bool has_user_save{user_save_size > 0};
  1930. const bool has_device_save{device_save_size > 0};
  1931. ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?");
  1932. switch (target) {
  1933. case GameListOpenTarget::SaveData: {
  1934. open_target = tr("Save Data");
  1935. const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
  1936. auto vfs_nand_dir =
  1937. vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);
  1938. if (has_user_save) {
  1939. // User save data
  1940. const auto select_profile = [this] {
  1941. const Core::Frontend::ProfileSelectParameters parameters{
  1942. .mode = Service::AM::Frontend::UiMode::UserSelector,
  1943. .invalid_uid_list = {},
  1944. .display_options = {},
  1945. .purpose = Service::AM::Frontend::UserSelectionPurpose::General,
  1946. };
  1947. QtProfileSelectionDialog dialog(*system, this, parameters);
  1948. dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
  1949. Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
  1950. dialog.setWindowModality(Qt::WindowModal);
  1951. if (dialog.exec() == QDialog::Rejected) {
  1952. return -1;
  1953. }
  1954. return dialog.GetIndex();
  1955. };
  1956. const auto index = select_profile();
  1957. if (index == -1) {
  1958. return;
  1959. }
  1960. const auto user_id =
  1961. system->GetProfileManager().GetUser(static_cast<std::size_t>(index));
  1962. ASSERT(user_id);
  1963. const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
  1964. {}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser,
  1965. FileSys::SaveDataType::SaveData, program_id, user_id->AsU128(), 0);
  1966. path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
  1967. } else {
  1968. // Device save data
  1969. const auto device_save_data_path = FileSys::SaveDataFactory::GetFullPath(
  1970. {}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser,
  1971. FileSys::SaveDataType::SaveData, program_id, {}, 0);
  1972. path = Common::FS::ConcatPathSafe(nand_dir, device_save_data_path);
  1973. }
  1974. if (!Common::FS::CreateDirs(path)) {
  1975. LOG_ERROR(Frontend, "Unable to create the directories for save data");
  1976. }
  1977. break;
  1978. }
  1979. case GameListOpenTarget::ModData: {
  1980. open_target = tr("Mod Data");
  1981. path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::LoadDir) /
  1982. fmt::format("{:016X}", program_id);
  1983. break;
  1984. }
  1985. default:
  1986. UNIMPLEMENTED();
  1987. break;
  1988. }
  1989. const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(path));
  1990. const QDir dir(qpath);
  1991. if (!dir.exists()) {
  1992. QMessageBox::warning(this, tr("Error Opening %1 Folder").arg(open_target),
  1993. tr("Folder does not exist!"));
  1994. return;
  1995. }
  1996. LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target.toStdString(),
  1997. program_id);
  1998. QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
  1999. }
  2000. void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) {
  2001. const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
  2002. const auto shader_cache_folder_path{shader_cache_dir / fmt::format("{:016x}", program_id)};
  2003. if (!Common::FS::CreateDirs(shader_cache_folder_path)) {
  2004. QMessageBox::warning(this, tr("Error Opening Transferable Shader Cache"),
  2005. tr("Failed to create the shader cache directory for this title."));
  2006. return;
  2007. }
  2008. const auto shader_path_string{Common::FS::PathToUTF8String(shader_cache_folder_path)};
  2009. const auto qt_shader_cache_path = QString::fromStdString(shader_path_string);
  2010. QDesktopServices::openUrl(QUrl::fromLocalFile(qt_shader_cache_path));
  2011. }
  2012. static bool RomFSRawCopy(size_t total_size, size_t& read_size, QProgressDialog& dialog,
  2013. const FileSys::VirtualDir& src, const FileSys::VirtualDir& dest,
  2014. bool full) {
  2015. if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
  2016. return false;
  2017. if (dialog.wasCanceled())
  2018. return false;
  2019. std::vector<u8> buffer(CopyBufferSize);
  2020. auto last_timestamp = std::chrono::steady_clock::now();
  2021. const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file,
  2022. const FileSys::VirtualFile& dest_file) {
  2023. if (src_file == nullptr || dest_file == nullptr) {
  2024. return false;
  2025. }
  2026. if (!dest_file->Resize(src_file->GetSize())) {
  2027. return false;
  2028. }
  2029. for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) {
  2030. if (dialog.wasCanceled()) {
  2031. dest_file->Resize(0);
  2032. return false;
  2033. }
  2034. using namespace std::literals::chrono_literals;
  2035. const auto new_timestamp = std::chrono::steady_clock::now();
  2036. if ((new_timestamp - last_timestamp) > 33ms) {
  2037. last_timestamp = new_timestamp;
  2038. dialog.setValue(
  2039. static_cast<int>(std::min(read_size, total_size) * 100 / total_size));
  2040. QCoreApplication::processEvents();
  2041. }
  2042. const auto read = src_file->Read(buffer.data(), buffer.size(), i);
  2043. dest_file->Write(buffer.data(), read, i);
  2044. read_size += read;
  2045. }
  2046. return true;
  2047. };
  2048. if (full) {
  2049. for (const auto& file : src->GetFiles()) {
  2050. const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName());
  2051. if (!QtRawCopy(file, out))
  2052. return false;
  2053. }
  2054. }
  2055. for (const auto& dir : src->GetSubdirectories()) {
  2056. const auto out = dest->CreateSubdirectory(dir->GetName());
  2057. if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full))
  2058. return false;
  2059. }
  2060. return true;
  2061. }
  2062. QString GMainWindow::GetGameListErrorRemoving(InstalledEntryType type) const {
  2063. switch (type) {
  2064. case InstalledEntryType::Game:
  2065. return tr("Error Removing Contents");
  2066. case InstalledEntryType::Update:
  2067. return tr("Error Removing Update");
  2068. case InstalledEntryType::AddOnContent:
  2069. return tr("Error Removing DLC");
  2070. default:
  2071. return QStringLiteral("Error Removing <Invalid Type>");
  2072. }
  2073. }
  2074. void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type) {
  2075. const QString entry_question = [type] {
  2076. switch (type) {
  2077. case InstalledEntryType::Game:
  2078. return tr("Remove Installed Game Contents?");
  2079. case InstalledEntryType::Update:
  2080. return tr("Remove Installed Game Update?");
  2081. case InstalledEntryType::AddOnContent:
  2082. return tr("Remove Installed Game DLC?");
  2083. default:
  2084. return QStringLiteral("Remove Installed Game <Invalid Type>?");
  2085. }
  2086. }();
  2087. if (!question(this, tr("Remove Entry"), entry_question, QMessageBox::Yes | QMessageBox::No,
  2088. QMessageBox::No)) {
  2089. return;
  2090. }
  2091. switch (type) {
  2092. case InstalledEntryType::Game:
  2093. RemoveBaseContent(program_id, type);
  2094. [[fallthrough]];
  2095. case InstalledEntryType::Update:
  2096. RemoveUpdateContent(program_id, type);
  2097. if (type != InstalledEntryType::Game) {
  2098. break;
  2099. }
  2100. [[fallthrough]];
  2101. case InstalledEntryType::AddOnContent:
  2102. RemoveAddOnContent(program_id, type);
  2103. break;
  2104. }
  2105. Common::FS::RemoveDirRecursively(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
  2106. "game_list");
  2107. game_list->PopulateAsync(UISettings::values.game_dirs);
  2108. }
  2109. void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
  2110. const auto res =
  2111. ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id);
  2112. if (res) {
  2113. QMessageBox::information(this, tr("Successfully Removed"),
  2114. tr("Successfully removed the installed base game."));
  2115. } else {
  2116. QMessageBox::warning(
  2117. this, GetGameListErrorRemoving(type),
  2118. tr("The base game is not installed in the NAND and cannot be removed."));
  2119. }
  2120. }
  2121. void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
  2122. const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
  2123. if (res) {
  2124. QMessageBox::information(this, tr("Successfully Removed"),
  2125. tr("Successfully removed the installed update."));
  2126. } else {
  2127. QMessageBox::warning(this, GetGameListErrorRemoving(type),
  2128. tr("There is no update installed for this title."));
  2129. }
  2130. }
  2131. void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
  2132. const size_t count = ContentManager::RemoveAllDLC(*system, program_id);
  2133. if (count == 0) {
  2134. QMessageBox::warning(this, GetGameListErrorRemoving(type),
  2135. tr("There are no DLC installed for this title."));
  2136. return;
  2137. }
  2138. QMessageBox::information(this, tr("Successfully Removed"),
  2139. tr("Successfully removed %1 installed DLC.").arg(count));
  2140. }
  2141. void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target,
  2142. const std::string& game_path) {
  2143. const QString question = [target] {
  2144. switch (target) {
  2145. case GameListRemoveTarget::GlShaderCache:
  2146. return tr("Delete OpenGL Transferable Shader Cache?");
  2147. case GameListRemoveTarget::VkShaderCache:
  2148. return tr("Delete Vulkan Transferable Shader Cache?");
  2149. case GameListRemoveTarget::AllShaderCache:
  2150. return tr("Delete All Transferable Shader Caches?");
  2151. case GameListRemoveTarget::CustomConfiguration:
  2152. return tr("Remove Custom Game Configuration?");
  2153. case GameListRemoveTarget::CacheStorage:
  2154. return tr("Remove Cache Storage?");
  2155. default:
  2156. return QString{};
  2157. }
  2158. }();
  2159. if (!GMainWindow::question(this, tr("Remove File"), question,
  2160. QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) {
  2161. return;
  2162. }
  2163. switch (target) {
  2164. case GameListRemoveTarget::VkShaderCache:
  2165. RemoveVulkanDriverPipelineCache(program_id);
  2166. [[fallthrough]];
  2167. case GameListRemoveTarget::GlShaderCache:
  2168. RemoveTransferableShaderCache(program_id, target);
  2169. break;
  2170. case GameListRemoveTarget::AllShaderCache:
  2171. RemoveAllTransferableShaderCaches(program_id);
  2172. break;
  2173. case GameListRemoveTarget::CustomConfiguration:
  2174. RemoveCustomConfiguration(program_id, game_path);
  2175. break;
  2176. case GameListRemoveTarget::CacheStorage:
  2177. RemoveCacheStorage(program_id);
  2178. break;
  2179. }
  2180. }
  2181. void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
  2182. if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
  2183. QMessageBox::Yes | QMessageBox::No,
  2184. QMessageBox::No) != QMessageBox::Yes) {
  2185. return;
  2186. }
  2187. play_time_manager->ResetProgramPlayTime(program_id);
  2188. game_list->PopulateAsync(UISettings::values.game_dirs);
  2189. }
  2190. void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) {
  2191. const auto target_file_name = [target] {
  2192. switch (target) {
  2193. case GameListRemoveTarget::GlShaderCache:
  2194. return "opengl.bin";
  2195. case GameListRemoveTarget::VkShaderCache:
  2196. return "vulkan.bin";
  2197. default:
  2198. return "";
  2199. }
  2200. }();
  2201. const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
  2202. const auto shader_cache_folder_path = shader_cache_dir / fmt::format("{:016x}", program_id);
  2203. const auto target_file = shader_cache_folder_path / target_file_name;
  2204. if (!Common::FS::Exists(target_file)) {
  2205. QMessageBox::warning(this, tr("Error Removing Transferable Shader Cache"),
  2206. tr("A shader cache for this title does not exist."));
  2207. return;
  2208. }
  2209. if (Common::FS::RemoveFile(target_file)) {
  2210. QMessageBox::information(this, tr("Successfully Removed"),
  2211. tr("Successfully removed the transferable shader cache."));
  2212. } else {
  2213. QMessageBox::warning(this, tr("Error Removing Transferable Shader Cache"),
  2214. tr("Failed to remove the transferable shader cache."));
  2215. }
  2216. }
  2217. void GMainWindow::RemoveVulkanDriverPipelineCache(u64 program_id) {
  2218. static constexpr std::string_view target_file_name = "vulkan_pipelines.bin";
  2219. const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
  2220. const auto shader_cache_folder_path = shader_cache_dir / fmt::format("{:016x}", program_id);
  2221. const auto target_file = shader_cache_folder_path / target_file_name;
  2222. if (!Common::FS::Exists(target_file)) {
  2223. return;
  2224. }
  2225. if (!Common::FS::RemoveFile(target_file)) {
  2226. QMessageBox::warning(this, tr("Error Removing Vulkan Driver Pipeline Cache"),
  2227. tr("Failed to remove the driver pipeline cache."));
  2228. }
  2229. }
  2230. void GMainWindow::RemoveAllTransferableShaderCaches(u64 program_id) {
  2231. const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
  2232. const auto program_shader_cache_dir = shader_cache_dir / fmt::format("{:016x}", program_id);
  2233. if (!Common::FS::Exists(program_shader_cache_dir)) {
  2234. QMessageBox::warning(this, tr("Error Removing Transferable Shader Caches"),
  2235. tr("A shader cache for this title does not exist."));
  2236. return;
  2237. }
  2238. if (Common::FS::RemoveDirRecursively(program_shader_cache_dir)) {
  2239. QMessageBox::information(this, tr("Successfully Removed"),
  2240. tr("Successfully removed the transferable shader caches."));
  2241. } else {
  2242. QMessageBox::warning(this, tr("Error Removing Transferable Shader Caches"),
  2243. tr("Failed to remove the transferable shader cache directory."));
  2244. }
  2245. }
  2246. void GMainWindow::RemoveCustomConfiguration(u64 program_id, const std::string& game_path) {
  2247. const auto file_path = std::filesystem::path(Common::FS::ToU8String(game_path));
  2248. const auto config_file_name =
  2249. program_id == 0 ? Common::FS::PathToUTF8String(file_path.filename()).append(".ini")
  2250. : fmt::format("{:016X}.ini", program_id);
  2251. const auto custom_config_file_path =
  2252. Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "custom" / config_file_name;
  2253. if (!Common::FS::Exists(custom_config_file_path)) {
  2254. QMessageBox::warning(this, tr("Error Removing Custom Configuration"),
  2255. tr("A custom configuration for this title does not exist."));
  2256. return;
  2257. }
  2258. if (Common::FS::RemoveFile(custom_config_file_path)) {
  2259. QMessageBox::information(this, tr("Successfully Removed"),
  2260. tr("Successfully removed the custom game configuration."));
  2261. } else {
  2262. QMessageBox::warning(this, tr("Error Removing Custom Configuration"),
  2263. tr("Failed to remove the custom game configuration."));
  2264. }
  2265. }
  2266. void GMainWindow::RemoveCacheStorage(u64 program_id) {
  2267. const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
  2268. auto vfs_nand_dir =
  2269. vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);
  2270. const auto cache_storage_path = FileSys::SaveDataFactory::GetFullPath(
  2271. {}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::CacheStorage,
  2272. 0 /* program_id */, {}, 0);
  2273. const auto path = Common::FS::ConcatPathSafe(nand_dir, cache_storage_path);
  2274. // Not an error if it wasn't cleared.
  2275. Common::FS::RemoveDirRecursively(path);
  2276. }
  2277. void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path,
  2278. DumpRomFSTarget target) {
  2279. const auto failed = [this] {
  2280. QMessageBox::warning(this, tr("RomFS Extraction Failed!"),
  2281. tr("There was an error copying the RomFS files or the user "
  2282. "cancelled the operation."));
  2283. };
  2284. const auto loader =
  2285. Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::OpenMode::Read));
  2286. if (loader == nullptr) {
  2287. failed();
  2288. return;
  2289. }
  2290. FileSys::VirtualFile packed_update_raw{};
  2291. loader->ReadUpdateRaw(packed_update_raw);
  2292. const auto& installed = system->GetContentProvider();
  2293. u64 title_id{};
  2294. u8 raw_type{};
  2295. if (!SelectRomFSDumpTarget(installed, program_id, &title_id, &raw_type)) {
  2296. failed();
  2297. return;
  2298. }
  2299. const auto type = static_cast<FileSys::ContentRecordType>(raw_type);
  2300. const auto base_nca = installed.GetEntry(title_id, type);
  2301. if (!base_nca) {
  2302. failed();
  2303. return;
  2304. }
  2305. const FileSys::NCA update_nca{packed_update_raw, nullptr};
  2306. if (type != FileSys::ContentRecordType::Program ||
  2307. update_nca.GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS ||
  2308. update_nca.GetTitleId() != FileSys::GetUpdateTitleID(title_id)) {
  2309. packed_update_raw = {};
  2310. }
  2311. const auto base_romfs = base_nca->GetRomFS();
  2312. const auto dump_dir =
  2313. target == DumpRomFSTarget::Normal
  2314. ? Common::FS::GetYuzuPath(Common::FS::YuzuPath::DumpDir)
  2315. : Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "atmosphere" / "contents";
  2316. const auto romfs_dir = fmt::format("{:016X}/romfs", title_id);
  2317. const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir);
  2318. const FileSys::PatchManager pm{title_id, system->GetFileSystemController(), installed};
  2319. auto romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, packed_update_raw, false);
  2320. const auto out = VfsFilesystemCreateDirectoryWrapper(vfs, path, FileSys::OpenMode::ReadWrite);
  2321. if (out == nullptr) {
  2322. failed();
  2323. vfs->DeleteDirectory(path);
  2324. return;
  2325. }
  2326. bool ok = false;
  2327. const QStringList selections{tr("Full"), tr("Skeleton")};
  2328. const auto res = QInputDialog::getItem(
  2329. this, tr("Select RomFS Dump Mode"),
  2330. tr("Please select the how you would like the RomFS dumped.<br>Full will copy all of the "
  2331. "files into the new directory while <br>skeleton will only create the directory "
  2332. "structure."),
  2333. selections, 0, false, &ok);
  2334. if (!ok) {
  2335. failed();
  2336. vfs->DeleteDirectory(path);
  2337. return;
  2338. }
  2339. const auto extracted = FileSys::ExtractRomFS(romfs);
  2340. if (extracted == nullptr) {
  2341. failed();
  2342. return;
  2343. }
  2344. const auto full = res == selections.constFirst();
  2345. // The expected required space is the size of the RomFS + 1 GiB
  2346. const auto minimum_free_space = romfs->GetSize() + 0x40000000;
  2347. if (full && Common::FS::GetFreeSpaceSize(path) < minimum_free_space) {
  2348. QMessageBox::warning(this, tr("RomFS Extraction Failed!"),
  2349. tr("There is not enough free space at %1 to extract the RomFS. Please "
  2350. "free up space or select a different dump directory at "
  2351. "Emulation > Configure > System > Filesystem > Dump Root")
  2352. .arg(QString::fromStdString(path)));
  2353. return;
  2354. }
  2355. QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, 100, this);
  2356. progress.setWindowModality(Qt::WindowModal);
  2357. progress.setMinimumDuration(100);
  2358. progress.setAutoClose(false);
  2359. progress.setAutoReset(false);
  2360. size_t read_size = 0;
  2361. if (RomFSRawCopy(romfs->GetSize(), read_size, progress, extracted, out, full)) {
  2362. progress.close();
  2363. QMessageBox::information(this, tr("RomFS Extraction Succeeded!"),
  2364. tr("The operation completed successfully."));
  2365. QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path)));
  2366. } else {
  2367. progress.close();
  2368. failed();
  2369. vfs->DeleteDirectory(path);
  2370. }
  2371. }
  2372. void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) {
  2373. const auto NotImplemented = [this] {
  2374. QMessageBox::warning(this, tr("Integrity verification couldn't be performed!"),
  2375. tr("File contents were not checked for validity."));
  2376. };
  2377. QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
  2378. progress.setWindowModality(Qt::WindowModal);
  2379. progress.setMinimumDuration(100);
  2380. progress.setAutoClose(false);
  2381. progress.setAutoReset(false);
  2382. const auto QtProgressCallback = [&](size_t total_size, size_t processed_size) {
  2383. progress.setValue(static_cast<int>((processed_size * 100) / total_size));
  2384. return progress.wasCanceled();
  2385. };
  2386. const auto result = ContentManager::VerifyGameContents(*system, game_path, QtProgressCallback);
  2387. progress.close();
  2388. switch (result) {
  2389. case ContentManager::GameVerificationResult::Success:
  2390. QMessageBox::information(this, tr("Integrity verification succeeded!"),
  2391. tr("The operation completed successfully."));
  2392. break;
  2393. case ContentManager::GameVerificationResult::Failed:
  2394. QMessageBox::critical(this, tr("Integrity verification failed!"),
  2395. tr("File contents may be corrupt."));
  2396. break;
  2397. case ContentManager::GameVerificationResult::NotImplemented:
  2398. NotImplemented();
  2399. }
  2400. }
  2401. void GMainWindow::OnGameListCopyTID(u64 program_id) {
  2402. QClipboard* clipboard = QGuiApplication::clipboard();
  2403. clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id)));
  2404. }
  2405. void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
  2406. const CompatibilityList& compatibility_list) {
  2407. const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
  2408. QString directory;
  2409. if (it != compatibility_list.end()) {
  2410. directory = it->second.second;
  2411. }
  2412. QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
  2413. }
  2414. bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path,
  2415. const std::string& comment,
  2416. const std::filesystem::path& icon_path,
  2417. const std::filesystem::path& command,
  2418. const std::string& arguments, const std::string& categories,
  2419. const std::string& keywords, const std::string& name) try {
  2420. #if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD
  2421. std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop");
  2422. std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc);
  2423. if (!shortcut_stream.is_open()) {
  2424. LOG_ERROR(Frontend, "Failed to create shortcut");
  2425. return false;
  2426. }
  2427. // TODO: Migrate fmt::print to std::print in futures STD C++ 23.
  2428. fmt::print(shortcut_stream, "[Desktop Entry]\n");
  2429. fmt::print(shortcut_stream, "Type=Application\n");
  2430. fmt::print(shortcut_stream, "Version=1.0\n");
  2431. fmt::print(shortcut_stream, "Name={}\n", name);
  2432. if (!comment.empty()) {
  2433. fmt::print(shortcut_stream, "Comment={}\n", comment);
  2434. }
  2435. if (std::filesystem::is_regular_file(icon_path)) {
  2436. fmt::print(shortcut_stream, "Icon={}\n", icon_path.string());
  2437. }
  2438. fmt::print(shortcut_stream, "TryExec={}\n", command.string());
  2439. fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments);
  2440. if (!categories.empty()) {
  2441. fmt::print(shortcut_stream, "Categories={}\n", categories);
  2442. }
  2443. if (!keywords.empty()) {
  2444. fmt::print(shortcut_stream, "Keywords={}\n", keywords);
  2445. }
  2446. return true;
  2447. #elif defined(_WIN32) // Windows
  2448. HRESULT hr = CoInitialize(nullptr);
  2449. if (FAILED(hr)) {
  2450. LOG_ERROR(Frontend, "CoInitialize failed");
  2451. return false;
  2452. }
  2453. SCOPE_EXIT({ CoUninitialize(); });
  2454. IShellLinkW* ps1 = nullptr;
  2455. IPersistFile* persist_file = nullptr;
  2456. SCOPE_EXIT({
  2457. if (persist_file != nullptr) {
  2458. persist_file->Release();
  2459. }
  2460. if (ps1 != nullptr) {
  2461. ps1->Release();
  2462. }
  2463. });
  2464. HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
  2465. reinterpret_cast<void**>(&ps1));
  2466. if (FAILED(hres)) {
  2467. LOG_ERROR(Frontend, "Failed to create IShellLinkW instance");
  2468. return false;
  2469. }
  2470. hres = ps1->SetPath(command.c_str());
  2471. if (FAILED(hres)) {
  2472. LOG_ERROR(Frontend, "Failed to set path");
  2473. return false;
  2474. }
  2475. if (!arguments.empty()) {
  2476. hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data());
  2477. if (FAILED(hres)) {
  2478. LOG_ERROR(Frontend, "Failed to set arguments");
  2479. return false;
  2480. }
  2481. }
  2482. if (!comment.empty()) {
  2483. hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data());
  2484. if (FAILED(hres)) {
  2485. LOG_ERROR(Frontend, "Failed to set description");
  2486. return false;
  2487. }
  2488. }
  2489. if (std::filesystem::is_regular_file(icon_path)) {
  2490. hres = ps1->SetIconLocation(icon_path.c_str(), 0);
  2491. if (FAILED(hres)) {
  2492. LOG_ERROR(Frontend, "Failed to set icon location");
  2493. return false;
  2494. }
  2495. }
  2496. hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&persist_file));
  2497. if (FAILED(hres)) {
  2498. LOG_ERROR(Frontend, "Failed to get IPersistFile interface");
  2499. return false;
  2500. }
  2501. hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE);
  2502. if (FAILED(hres)) {
  2503. LOG_ERROR(Frontend, "Failed to save shortcut");
  2504. return false;
  2505. }
  2506. return true;
  2507. #else // Unsupported platform
  2508. return false;
  2509. #endif
  2510. } catch (const std::exception& e) {
  2511. LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what());
  2512. return false;
  2513. }
  2514. // Messages in pre-defined message boxes for less code spaghetti
  2515. bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title) {
  2516. int result = 0;
  2517. QMessageBox::StandardButtons buttons;
  2518. switch (imsg) {
  2519. case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES:
  2520. buttons = QMessageBox::Yes | QMessageBox::No;
  2521. result =
  2522. QMessageBox::information(parent, tr("Create Shortcut"),
  2523. tr("Do you want to launch the game in fullscreen?"), buttons);
  2524. return result == QMessageBox::Yes;
  2525. case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS:
  2526. QMessageBox::information(parent, tr("Create Shortcut"),
  2527. tr("Successfully created a shortcut to %1").arg(game_title));
  2528. return false;
  2529. case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING:
  2530. buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel;
  2531. result =
  2532. QMessageBox::warning(this, tr("Create Shortcut"),
  2533. tr("This will create a shortcut to the current AppImage. This may "
  2534. "not work well if you update. Continue?"),
  2535. buttons);
  2536. return result == QMessageBox::Ok;
  2537. default:
  2538. buttons = QMessageBox::Ok;
  2539. QMessageBox::critical(parent, tr("Create Shortcut"),
  2540. tr("Failed to create a shortcut to %1").arg(game_title), buttons);
  2541. return false;
  2542. }
  2543. }
  2544. bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name,
  2545. std::filesystem::path& out_icon_path) {
  2546. // Get path to Yuzu icons directory & icon extension
  2547. std::string ico_extension = "png";
  2548. #if defined(_WIN32)
  2549. out_icon_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir);
  2550. ico_extension = "ico";
  2551. #elif defined(__linux__) || defined(__FreeBSD__)
  2552. out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256";
  2553. #endif
  2554. // Create icons directory if it doesn't exist
  2555. if (!Common::FS::CreateDirs(out_icon_path)) {
  2556. QMessageBox::critical(
  2557. this, tr("Create Icon"),
  2558. tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.")
  2559. .arg(QString::fromStdString(out_icon_path.string())),
  2560. QMessageBox::StandardButton::Ok);
  2561. out_icon_path.clear();
  2562. return false;
  2563. }
  2564. // Create icon file path
  2565. out_icon_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension)
  2566. : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension));
  2567. return true;
  2568. }
  2569. void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
  2570. GameListShortcutTarget target) {
  2571. std::string game_title;
  2572. QString qt_game_title;
  2573. std::filesystem::path out_icon_path;
  2574. // Get path to yuzu executable
  2575. const QStringList args = QApplication::arguments();
  2576. std::filesystem::path yuzu_command = args[0].toStdString();
  2577. // If relative path, make it an absolute path
  2578. if (yuzu_command.c_str()[0] == '.') {
  2579. yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
  2580. }
  2581. // Shortcut path
  2582. std::filesystem::path shortcut_path{};
  2583. if (target == GameListShortcutTarget::Desktop) {
  2584. shortcut_path =
  2585. QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString();
  2586. } else if (target == GameListShortcutTarget::Applications) {
  2587. shortcut_path =
  2588. QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation).toStdString();
  2589. }
  2590. // Icon path and title
  2591. if (std::filesystem::exists(shortcut_path)) {
  2592. // Get title from game file
  2593. const FileSys::PatchManager pm{program_id, system->GetFileSystemController(),
  2594. system->GetContentProvider()};
  2595. const auto control = pm.GetControlMetadata();
  2596. const auto loader =
  2597. Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::OpenMode::Read));
  2598. game_title = fmt::format("{:016X}", program_id);
  2599. if (control.first != nullptr) {
  2600. game_title = control.first->GetApplicationName();
  2601. } else {
  2602. loader->ReadTitle(game_title);
  2603. }
  2604. // Delete illegal characters from title
  2605. const std::string illegal_chars = "<>:\"/\\|?*.";
  2606. for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) {
  2607. if (illegal_chars.find(*it) != std::string::npos) {
  2608. game_title.erase(it.base() - 1);
  2609. }
  2610. }
  2611. qt_game_title = QString::fromStdString(game_title);
  2612. // Get icon from game file
  2613. std::vector<u8> icon_image_file{};
  2614. if (control.second != nullptr) {
  2615. icon_image_file = control.second->ReadAllBytes();
  2616. } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) {
  2617. LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
  2618. }
  2619. QImage icon_data =
  2620. QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
  2621. if (GMainWindow::MakeShortcutIcoPath(program_id, game_title, out_icon_path)) {
  2622. if (!SaveIconToFile(out_icon_path, icon_data)) {
  2623. LOG_ERROR(Frontend, "Could not write icon to file");
  2624. }
  2625. }
  2626. } else {
  2627. GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR,
  2628. qt_game_title);
  2629. LOG_ERROR(Frontend, "Invalid shortcut target");
  2630. return;
  2631. }
  2632. #if defined(__linux__)
  2633. // Special case for AppImages
  2634. // Warn once if we are making a shortcut to a volatile AppImage
  2635. const std::string appimage_ending =
  2636. std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage");
  2637. if (yuzu_command.string().ends_with(appimage_ending) &&
  2638. !UISettings::values.shortcut_already_warned) {
  2639. if (GMainWindow::CreateShortcutMessagesGUI(
  2640. this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, qt_game_title)) {
  2641. return;
  2642. }
  2643. UISettings::values.shortcut_already_warned = true;
  2644. }
  2645. #endif // __linux__
  2646. // Create shortcut
  2647. std::string arguments = fmt::format("-g \"{:s}\"", game_path);
  2648. if (GMainWindow::CreateShortcutMessagesGUI(
  2649. this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, qt_game_title)) {
  2650. arguments = "-f " + arguments;
  2651. }
  2652. const std::string comment = fmt::format("Start {:s} with the yuzu Emulator", game_title);
  2653. const std::string categories = "Game;Emulator;Qt;";
  2654. const std::string keywords = "Switch;Nintendo;";
  2655. if (GMainWindow::CreateShortcutLink(shortcut_path, comment, out_icon_path, yuzu_command,
  2656. arguments, categories, keywords, game_title)) {
  2657. GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS,
  2658. qt_game_title);
  2659. return;
  2660. }
  2661. GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR,
  2662. qt_game_title);
  2663. }
  2664. void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
  2665. std::filesystem::path fs_path;
  2666. if (directory == QStringLiteral("SDMC")) {
  2667. fs_path =
  2668. Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "Nintendo/Contents/registered";
  2669. } else if (directory == QStringLiteral("UserNAND")) {
  2670. fs_path =
  2671. Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "user/Contents/registered";
  2672. } else if (directory == QStringLiteral("SysNAND")) {
  2673. fs_path =
  2674. Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/Contents/registered";
  2675. } else {
  2676. fs_path = directory.toStdString();
  2677. }
  2678. const auto qt_path = QString::fromStdString(Common::FS::PathToUTF8String(fs_path));
  2679. if (!Common::FS::IsDir(fs_path)) {
  2680. QMessageBox::critical(this, tr("Error Opening %1").arg(qt_path),
  2681. tr("Folder does not exist!"));
  2682. return;
  2683. }
  2684. QDesktopServices::openUrl(QUrl::fromLocalFile(qt_path));
  2685. }
  2686. void GMainWindow::OnGameListAddDirectory() {
  2687. const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
  2688. if (dir_path.isEmpty()) {
  2689. return;
  2690. }
  2691. UISettings::GameDir game_dir{dir_path.toStdString(), false, true};
  2692. if (!UISettings::values.game_dirs.contains(game_dir)) {
  2693. UISettings::values.game_dirs.append(game_dir);
  2694. game_list->PopulateAsync(UISettings::values.game_dirs);
  2695. } else {
  2696. LOG_WARNING(Frontend, "Selected directory is already in the game list");
  2697. }
  2698. OnSaveConfig();
  2699. }
  2700. void GMainWindow::OnGameListShowList(bool show) {
  2701. if (emulation_running && ui->action_Single_Window_Mode->isChecked())
  2702. return;
  2703. game_list->setVisible(show);
  2704. game_list_placeholder->setVisible(!show);
  2705. };
  2706. void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
  2707. u64 title_id{};
  2708. const auto v_file = Core::GetGameFileFromPath(vfs, file);
  2709. const auto loader = Loader::GetLoader(*system, v_file);
  2710. if (loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
  2711. QMessageBox::information(this, tr("Properties"),
  2712. tr("The game properties could not be loaded."));
  2713. return;
  2714. }
  2715. OpenPerGameConfiguration(title_id, file);
  2716. }
  2717. void GMainWindow::OnMenuLoadFile() {
  2718. if (is_load_file_select_active) {
  2719. return;
  2720. }
  2721. is_load_file_select_active = true;
  2722. const QString extensions =
  2723. QStringLiteral("*.")
  2724. .append(GameList::supported_file_extensions.join(QStringLiteral(" *.")))
  2725. .append(QStringLiteral(" main"));
  2726. const QString file_filter = tr("Switch Executable (%1);;All Files (*.*)",
  2727. "%1 is an identifier for the Switch executable file extensions.")
  2728. .arg(extensions);
  2729. const QString filename = QFileDialog::getOpenFileName(
  2730. this, tr("Load File"), QString::fromStdString(UISettings::values.roms_path), file_filter);
  2731. is_load_file_select_active = false;
  2732. if (filename.isEmpty()) {
  2733. return;
  2734. }
  2735. UISettings::values.roms_path = QFileInfo(filename).path().toStdString();
  2736. BootGame(filename, ApplicationAppletParameters());
  2737. }
  2738. void GMainWindow::OnMenuLoadFolder() {
  2739. const QString dir_path =
  2740. QFileDialog::getExistingDirectory(this, tr("Open Extracted ROM Directory"));
  2741. if (dir_path.isNull()) {
  2742. return;
  2743. }
  2744. const QDir dir{dir_path};
  2745. const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
  2746. if (matching_main.size() == 1) {
  2747. BootGame(dir.path() + QDir::separator() + matching_main[0], ApplicationAppletParameters());
  2748. } else {
  2749. QMessageBox::warning(this, tr("Invalid Directory Selected"),
  2750. tr("The directory you have selected does not contain a 'main' file."));
  2751. }
  2752. }
  2753. void GMainWindow::IncrementInstallProgress() {
  2754. install_progress->setValue(install_progress->value() + 1);
  2755. }
  2756. void GMainWindow::OnMenuInstallToNAND() {
  2757. const QString file_filter =
  2758. tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
  2759. "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
  2760. "Image (*.xci)");
  2761. QStringList filenames = QFileDialog::getOpenFileNames(
  2762. this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
  2763. file_filter);
  2764. if (filenames.isEmpty()) {
  2765. return;
  2766. }
  2767. InstallDialog installDialog(this, filenames);
  2768. if (installDialog.exec() == QDialog::Rejected) {
  2769. return;
  2770. }
  2771. const QStringList files = installDialog.GetFiles();
  2772. if (files.isEmpty()) {
  2773. return;
  2774. }
  2775. // Save folder location of the first selected file
  2776. UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString();
  2777. int remaining = filenames.size();
  2778. // This would only overflow above 2^51 bytes (2.252 PB)
  2779. int total_size = 0;
  2780. for (const QString& file : files) {
  2781. total_size += static_cast<int>(QFile(file).size() / CopyBufferSize);
  2782. }
  2783. if (total_size < 0) {
  2784. LOG_CRITICAL(Frontend, "Attempting to install too many files, aborting.");
  2785. return;
  2786. }
  2787. QStringList new_files{}; // Newly installed files that do not yet exist in the NAND
  2788. QStringList overwritten_files{}; // Files that overwrote those existing in the NAND
  2789. QStringList failed_files{}; // Files that failed to install due to errors
  2790. bool detected_base_install{}; // Whether a base game was attempted to be installed
  2791. ui->action_Install_File_NAND->setEnabled(false);
  2792. install_progress = new QProgressDialog(QString{}, tr("Cancel"), 0, total_size, this);
  2793. install_progress->setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint);
  2794. install_progress->setAttribute(Qt::WA_DeleteOnClose, true);
  2795. install_progress->setFixedWidth(installDialog.GetMinimumWidth() + 40);
  2796. install_progress->show();
  2797. for (const QString& file : files) {
  2798. install_progress->setWindowTitle(tr("%n file(s) remaining", "", remaining));
  2799. install_progress->setLabelText(
  2800. tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
  2801. QFuture<ContentManager::InstallResult> future;
  2802. ContentManager::InstallResult result;
  2803. if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
  2804. const auto progress_callback = [this](size_t size, size_t progress) {
  2805. emit UpdateInstallProgress();
  2806. if (install_progress->wasCanceled()) {
  2807. return true;
  2808. }
  2809. return false;
  2810. };
  2811. future = QtConcurrent::run([this, &file, progress_callback] {
  2812. return ContentManager::InstallNSP(*system, *vfs, file.toStdString(),
  2813. progress_callback);
  2814. });
  2815. while (!future.isFinished()) {
  2816. QCoreApplication::processEvents();
  2817. std::this_thread::sleep_for(std::chrono::milliseconds(1));
  2818. }
  2819. result = future.result();
  2820. } else {
  2821. result = InstallNCA(file);
  2822. }
  2823. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  2824. switch (result) {
  2825. case ContentManager::InstallResult::Success:
  2826. new_files.append(QFileInfo(file).fileName());
  2827. break;
  2828. case ContentManager::InstallResult::Overwrite:
  2829. overwritten_files.append(QFileInfo(file).fileName());
  2830. break;
  2831. case ContentManager::InstallResult::Failure:
  2832. failed_files.append(QFileInfo(file).fileName());
  2833. break;
  2834. case ContentManager::InstallResult::BaseInstallAttempted:
  2835. failed_files.append(QFileInfo(file).fileName());
  2836. detected_base_install = true;
  2837. break;
  2838. }
  2839. --remaining;
  2840. }
  2841. install_progress->close();
  2842. if (detected_base_install) {
  2843. QMessageBox::warning(
  2844. this, tr("Install Results"),
  2845. tr("To avoid possible conflicts, we discourage users from installing base games to the "
  2846. "NAND.\nPlease, only use this feature to install updates and DLC."));
  2847. }
  2848. const QString install_results =
  2849. (new_files.isEmpty() ? QString{}
  2850. : tr("%n file(s) were newly installed\n", "", new_files.size())) +
  2851. (overwritten_files.isEmpty()
  2852. ? QString{}
  2853. : tr("%n file(s) were overwritten\n", "", overwritten_files.size())) +
  2854. (failed_files.isEmpty() ? QString{}
  2855. : tr("%n file(s) failed to install\n", "", failed_files.size()));
  2856. QMessageBox::information(this, tr("Install Results"), install_results);
  2857. Common::FS::RemoveDirRecursively(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
  2858. "game_list");
  2859. game_list->PopulateAsync(UISettings::values.game_dirs);
  2860. ui->action_Install_File_NAND->setEnabled(true);
  2861. }
  2862. ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) {
  2863. const QStringList tt_options{tr("System Application"),
  2864. tr("System Archive"),
  2865. tr("System Application Update"),
  2866. tr("Firmware Package (Type A)"),
  2867. tr("Firmware Package (Type B)"),
  2868. tr("Game"),
  2869. tr("Game Update"),
  2870. tr("Game DLC"),
  2871. tr("Delta Title")};
  2872. bool ok;
  2873. const auto item = QInputDialog::getItem(
  2874. this, tr("Select NCA Install Type..."),
  2875. tr("Please select the type of title you would like to install this NCA as:\n(In "
  2876. "most instances, the default 'Game' is fine.)"),
  2877. tt_options, 5, false, &ok);
  2878. auto index = tt_options.indexOf(item);
  2879. if (!ok || index == -1) {
  2880. QMessageBox::warning(this, tr("Failed to Install"),
  2881. tr("The title type you selected for the NCA is invalid."));
  2882. return ContentManager::InstallResult::Failure;
  2883. }
  2884. // If index is equal to or past Game, add the jump in TitleType.
  2885. if (index >= 5) {
  2886. index += static_cast<size_t>(FileSys::TitleType::Application) -
  2887. static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
  2888. }
  2889. const bool is_application = index >= static_cast<s32>(FileSys::TitleType::Application);
  2890. const auto& fs_controller = system->GetFileSystemController();
  2891. auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
  2892. : fs_controller.GetSystemNANDContents();
  2893. const auto progress_callback = [this](size_t size, size_t progress) {
  2894. emit UpdateInstallProgress();
  2895. if (install_progress->wasCanceled()) {
  2896. return true;
  2897. }
  2898. return false;
  2899. };
  2900. return ContentManager::InstallNCA(*vfs, filename.toStdString(), *registered_cache,
  2901. static_cast<FileSys::TitleType>(index), progress_callback);
  2902. }
  2903. void GMainWindow::OnMenuRecentFile() {
  2904. QAction* action = qobject_cast<QAction*>(sender());
  2905. assert(action);
  2906. const QString filename = action->data().toString();
  2907. if (QFileInfo::exists(filename)) {
  2908. BootGame(filename, ApplicationAppletParameters());
  2909. } else {
  2910. // Display an error message and remove the file from the list.
  2911. QMessageBox::information(this, tr("File not found"),
  2912. tr("File \"%1\" not found").arg(filename));
  2913. UISettings::values.recent_files.removeOne(filename);
  2914. UpdateRecentFiles();
  2915. }
  2916. }
  2917. void GMainWindow::OnStartGame() {
  2918. PreventOSSleep();
  2919. emu_thread->SetRunning(true);
  2920. UpdateMenuState();
  2921. OnTasStateChanged();
  2922. play_time_manager->SetProgramId(system->GetApplicationProcessProgramID());
  2923. play_time_manager->Start();
  2924. discord_rpc->Update();
  2925. #ifdef __unix__
  2926. Common::Linux::StartGamemode();
  2927. #endif
  2928. }
  2929. void GMainWindow::OnRestartGame() {
  2930. if (!system->IsPoweredOn()) {
  2931. return;
  2932. }
  2933. if (ConfirmShutdownGame()) {
  2934. // Make a copy since ShutdownGame edits game_path
  2935. const auto current_game = QString(current_game_path);
  2936. ShutdownGame();
  2937. BootGame(current_game, ApplicationAppletParameters());
  2938. }
  2939. }
  2940. void GMainWindow::OnPauseGame() {
  2941. emu_thread->SetRunning(false);
  2942. play_time_manager->Stop();
  2943. UpdateMenuState();
  2944. AllowOSSleep();
  2945. #ifdef __unix__
  2946. Common::Linux::StopGamemode();
  2947. #endif
  2948. }
  2949. void GMainWindow::OnPauseContinueGame() {
  2950. if (emulation_running) {
  2951. if (emu_thread->IsRunning()) {
  2952. OnPauseGame();
  2953. } else {
  2954. RequestGameResume();
  2955. OnStartGame();
  2956. }
  2957. }
  2958. }
  2959. void GMainWindow::OnStopGame() {
  2960. if (ConfirmShutdownGame()) {
  2961. play_time_manager->Stop();
  2962. // Update game list to show new play time
  2963. game_list->PopulateAsync(UISettings::values.game_dirs);
  2964. if (OnShutdownBegin()) {
  2965. OnShutdownBeginDialog();
  2966. } else {
  2967. OnEmulationStopped();
  2968. }
  2969. }
  2970. }
  2971. bool GMainWindow::ConfirmShutdownGame() {
  2972. if (UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Always) {
  2973. if (system->GetExitLocked()) {
  2974. if (!ConfirmForceLockedExit()) {
  2975. return false;
  2976. }
  2977. } else {
  2978. if (!ConfirmChangeGame()) {
  2979. return false;
  2980. }
  2981. }
  2982. } else {
  2983. if (UISettings::values.confirm_before_stopping.GetValue() ==
  2984. ConfirmStop::Ask_Based_On_Game &&
  2985. system->GetExitLocked()) {
  2986. if (!ConfirmForceLockedExit()) {
  2987. return false;
  2988. }
  2989. }
  2990. }
  2991. return true;
  2992. }
  2993. void GMainWindow::OnLoadComplete() {
  2994. loading_screen->OnLoadComplete();
  2995. }
  2996. void GMainWindow::OnExecuteProgram(std::size_t program_index) {
  2997. ShutdownGame();
  2998. auto params = ApplicationAppletParameters();
  2999. params.program_index = static_cast<s32>(program_index);
  3000. params.launch_type = Service::AM::LaunchType::ApplicationInitiated;
  3001. BootGame(last_filename_booted, params);
  3002. }
  3003. void GMainWindow::OnExit() {
  3004. ShutdownGame();
  3005. }
  3006. void GMainWindow::OnSaveConfig() {
  3007. system->ApplySettings();
  3008. config->SaveAllValues();
  3009. }
  3010. void GMainWindow::ErrorDisplayDisplayError(QString error_code, QString error_text) {
  3011. error_applet = new OverlayDialog(render_window, *system, error_code, error_text, QString{},
  3012. tr("OK"), Qt::AlignLeft | Qt::AlignVCenter);
  3013. SCOPE_EXIT({
  3014. error_applet->deleteLater();
  3015. error_applet = nullptr;
  3016. });
  3017. error_applet->exec();
  3018. emit ErrorDisplayFinished();
  3019. }
  3020. void GMainWindow::ErrorDisplayRequestExit() {
  3021. if (error_applet) {
  3022. error_applet->reject();
  3023. }
  3024. }
  3025. void GMainWindow::OnMenuReportCompatibility() {
  3026. #if defined(ARCHITECTURE_x86_64) && !defined(__APPLE__)
  3027. const auto& caps = Common::GetCPUCaps();
  3028. const bool has_fma = caps.fma || caps.fma4;
  3029. const auto processor_count = std::thread::hardware_concurrency();
  3030. const bool has_4threads = processor_count == 0 || processor_count >= 4;
  3031. const bool has_8gb_ram = Common::GetMemInfo().TotalPhysicalMemory >= 8_GiB;
  3032. const bool has_broken_vulkan = UISettings::values.has_broken_vulkan;
  3033. if (!has_fma || !has_4threads || !has_8gb_ram || has_broken_vulkan) {
  3034. QMessageBox::critical(this, tr("Hardware requirements not met"),
  3035. tr("Your system does not meet the recommended hardware requirements. "
  3036. "Compatibility reporting has been disabled."));
  3037. return;
  3038. }
  3039. if (!Settings::values.yuzu_token.GetValue().empty() &&
  3040. !Settings::values.yuzu_username.GetValue().empty()) {
  3041. CompatDB compatdb{system->TelemetrySession(), this};
  3042. compatdb.exec();
  3043. } else {
  3044. QMessageBox::critical(
  3045. this, tr("Missing yuzu Account"),
  3046. tr("In order to submit a game compatibility test case, you must link your yuzu "
  3047. "account.<br><br/>To link your yuzu account, go to Emulation &gt; Configuration "
  3048. "&gt; "
  3049. "Web."));
  3050. }
  3051. #else
  3052. QMessageBox::critical(this, tr("Hardware requirements not met"),
  3053. tr("Your system does not meet the recommended hardware requirements. "
  3054. "Compatibility reporting has been disabled."));
  3055. #endif
  3056. }
  3057. void GMainWindow::OpenURL(const QUrl& url) {
  3058. const bool open = QDesktopServices::openUrl(url);
  3059. if (!open) {
  3060. QMessageBox::warning(this, tr("Error opening URL"),
  3061. tr("Unable to open the URL \"%1\".").arg(url.toString()));
  3062. }
  3063. }
  3064. void GMainWindow::OnOpenModsPage() {
  3065. OpenURL(QUrl(QStringLiteral("https://github.com/yuzu-emu/yuzu/wiki/Switch-Mods")));
  3066. }
  3067. void GMainWindow::OnOpenQuickstartGuide() {
  3068. OpenURL(QUrl(QStringLiteral("https://yuzu-emu.org/help/quickstart/")));
  3069. }
  3070. void GMainWindow::OnOpenFAQ() {
  3071. OpenURL(QUrl(QStringLiteral("https://yuzu-emu.org/wiki/faq/")));
  3072. }
  3073. void GMainWindow::ToggleFullscreen() {
  3074. if (!emulation_running) {
  3075. return;
  3076. }
  3077. if (ui->action_Fullscreen->isChecked()) {
  3078. ShowFullscreen();
  3079. } else {
  3080. HideFullscreen();
  3081. }
  3082. }
  3083. // We're going to return the screen that the given window has the most pixels on
  3084. static QScreen* GuessCurrentScreen(QWidget* window) {
  3085. const QList<QScreen*> screens = QGuiApplication::screens();
  3086. return *std::max_element(
  3087. screens.cbegin(), screens.cend(), [window](const QScreen* left, const QScreen* right) {
  3088. const QSize left_size = left->geometry().intersected(window->geometry()).size();
  3089. const QSize right_size = right->geometry().intersected(window->geometry()).size();
  3090. return (left_size.height() * left_size.width()) <
  3091. (right_size.height() * right_size.width());
  3092. });
  3093. }
  3094. bool GMainWindow::UsingExclusiveFullscreen() {
  3095. return Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive ||
  3096. QGuiApplication::platformName() == QStringLiteral("wayland") ||
  3097. QGuiApplication::platformName() == QStringLiteral("wayland-egl");
  3098. }
  3099. void GMainWindow::ShowFullscreen() {
  3100. const auto show_fullscreen = [this](QWidget* window) {
  3101. if (UsingExclusiveFullscreen()) {
  3102. window->showFullScreen();
  3103. return;
  3104. }
  3105. window->hide();
  3106. window->setWindowFlags(window->windowFlags() | Qt::FramelessWindowHint);
  3107. const auto screen_geometry = GuessCurrentScreen(window)->geometry();
  3108. window->setGeometry(screen_geometry.x(), screen_geometry.y(), screen_geometry.width(),
  3109. screen_geometry.height() + 1);
  3110. window->raise();
  3111. window->showNormal();
  3112. };
  3113. if (ui->action_Single_Window_Mode->isChecked()) {
  3114. UISettings::values.geometry = saveGeometry();
  3115. ui->menubar->hide();
  3116. statusBar()->hide();
  3117. show_fullscreen(this);
  3118. } else {
  3119. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  3120. show_fullscreen(render_window);
  3121. }
  3122. }
  3123. void GMainWindow::HideFullscreen() {
  3124. if (ui->action_Single_Window_Mode->isChecked()) {
  3125. if (UsingExclusiveFullscreen()) {
  3126. showNormal();
  3127. restoreGeometry(UISettings::values.geometry);
  3128. } else {
  3129. hide();
  3130. setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
  3131. restoreGeometry(UISettings::values.geometry);
  3132. raise();
  3133. show();
  3134. }
  3135. statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
  3136. ui->menubar->show();
  3137. } else {
  3138. if (UsingExclusiveFullscreen()) {
  3139. render_window->showNormal();
  3140. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  3141. } else {
  3142. render_window->hide();
  3143. render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
  3144. render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
  3145. render_window->raise();
  3146. render_window->show();
  3147. }
  3148. }
  3149. }
  3150. void GMainWindow::ToggleWindowMode() {
  3151. if (ui->action_Single_Window_Mode->isChecked()) {
  3152. // Render in the main window...
  3153. render_window->BackupGeometry();
  3154. ui->horizontalLayout->addWidget(render_window);
  3155. render_window->setFocusPolicy(Qt::StrongFocus);
  3156. if (emulation_running) {
  3157. render_window->setVisible(true);
  3158. render_window->setFocus();
  3159. game_list->hide();
  3160. }
  3161. } else {
  3162. // Render in a separate window...
  3163. ui->horizontalLayout->removeWidget(render_window);
  3164. render_window->setParent(nullptr);
  3165. render_window->setFocusPolicy(Qt::NoFocus);
  3166. if (emulation_running) {
  3167. render_window->setVisible(true);
  3168. render_window->RestoreGeometry();
  3169. game_list->show();
  3170. }
  3171. }
  3172. }
  3173. void GMainWindow::ResetWindowSize(u32 width, u32 height) {
  3174. const auto aspect_ratio = Layout::EmulationAspectRatio(
  3175. static_cast<Layout::AspectRatio>(Settings::values.aspect_ratio.GetValue()),
  3176. static_cast<float>(height) / width);
  3177. if (!ui->action_Single_Window_Mode->isChecked()) {
  3178. render_window->resize(height / aspect_ratio, height);
  3179. } else {
  3180. const bool show_status_bar = ui->action_Show_Status_Bar->isChecked();
  3181. const auto status_bar_height = show_status_bar ? statusBar()->height() : 0;
  3182. resize(height / aspect_ratio, height + menuBar()->height() + status_bar_height);
  3183. }
  3184. }
  3185. void GMainWindow::ResetWindowSize720() {
  3186. ResetWindowSize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
  3187. }
  3188. void GMainWindow::ResetWindowSize900() {
  3189. ResetWindowSize(1600U, 900U);
  3190. }
  3191. void GMainWindow::ResetWindowSize1080() {
  3192. ResetWindowSize(Layout::ScreenDocked::Width, Layout::ScreenDocked::Height);
  3193. }
  3194. void GMainWindow::OnConfigure() {
  3195. const auto old_theme = UISettings::values.theme;
  3196. const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
  3197. const auto old_language_index = Settings::values.language_index.GetValue();
  3198. #ifdef __unix__
  3199. const bool old_gamemode = Settings::values.enable_gamemode.GetValue();
  3200. #endif
  3201. Settings::SetConfiguringGlobal(true);
  3202. ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(),
  3203. vk_device_records, *system,
  3204. !multiplayer_state->IsHostingPublicRoom());
  3205. connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this,
  3206. &GMainWindow::OnLanguageChanged);
  3207. const auto result = configure_dialog.exec();
  3208. if (result != QDialog::Accepted && !UISettings::values.configuration_applied &&
  3209. !UISettings::values.reset_to_defaults) {
  3210. // Runs if the user hit Cancel or closed the window, and did not ever press the Apply button
  3211. // or `Reset to Defaults` button
  3212. return;
  3213. } else if (result == QDialog::Accepted) {
  3214. // Only apply new changes if user hit Okay
  3215. // This is here to avoid applying changes if the user hit Apply, made some changes, then hit
  3216. // Cancel
  3217. configure_dialog.ApplyConfiguration();
  3218. } else if (UISettings::values.reset_to_defaults) {
  3219. LOG_INFO(Frontend, "Resetting all settings to defaults");
  3220. if (!Common::FS::RemoveFile(config->GetConfigFilePath())) {
  3221. LOG_WARNING(Frontend, "Failed to remove configuration file");
  3222. }
  3223. if (!Common::FS::RemoveDirContentsRecursively(
  3224. Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "custom")) {
  3225. LOG_WARNING(Frontend, "Failed to remove custom configuration files");
  3226. }
  3227. if (!Common::FS::RemoveDirRecursively(
  3228. Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "game_list")) {
  3229. LOG_WARNING(Frontend, "Failed to remove game metadata cache files");
  3230. }
  3231. // Explicitly save the game directories, since reinitializing config does not explicitly do
  3232. // so.
  3233. QVector<UISettings::GameDir> old_game_dirs = std::move(UISettings::values.game_dirs);
  3234. QVector<u64> old_favorited_ids = std::move(UISettings::values.favorited_ids);
  3235. Settings::values.disabled_addons.clear();
  3236. config = std::make_unique<QtConfig>();
  3237. UISettings::values.reset_to_defaults = false;
  3238. UISettings::values.game_dirs = std::move(old_game_dirs);
  3239. UISettings::values.favorited_ids = std::move(old_favorited_ids);
  3240. InitializeRecentFileMenuActions();
  3241. SetDefaultUIGeometry();
  3242. RestoreUIState();
  3243. ShowTelemetryCallout();
  3244. }
  3245. InitializeHotkeys();
  3246. if (UISettings::values.theme != old_theme) {
  3247. UpdateUITheme();
  3248. }
  3249. if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) {
  3250. SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
  3251. }
  3252. #ifdef __unix__
  3253. if (Settings::values.enable_gamemode.GetValue() != old_gamemode) {
  3254. SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
  3255. }
  3256. #endif
  3257. if (!multiplayer_state->IsHostingPublicRoom()) {
  3258. multiplayer_state->UpdateCredentials();
  3259. }
  3260. emit UpdateThemedIcons();
  3261. const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
  3262. if (reload || Settings::values.language_index.GetValue() != old_language_index) {
  3263. game_list->PopulateAsync(UISettings::values.game_dirs);
  3264. }
  3265. UISettings::values.configuration_applied = false;
  3266. config->SaveAllValues();
  3267. if ((UISettings::values.hide_mouse || Settings::values.mouse_panning) && emulation_running) {
  3268. render_window->installEventFilter(render_window);
  3269. render_window->setAttribute(Qt::WA_Hover, true);
  3270. } else {
  3271. render_window->removeEventFilter(render_window);
  3272. render_window->setAttribute(Qt::WA_Hover, false);
  3273. }
  3274. if (UISettings::values.hide_mouse) {
  3275. mouse_hide_timer.start();
  3276. }
  3277. // Restart camera config
  3278. if (emulation_running) {
  3279. render_window->FinalizeCamera();
  3280. render_window->InitializeCamera();
  3281. }
  3282. if (!UISettings::values.has_broken_vulkan) {
  3283. renderer_status_button->setEnabled(!emulation_running);
  3284. }
  3285. UpdateStatusButtons();
  3286. controller_dialog->refreshConfiguration();
  3287. system->ApplySettings();
  3288. }
  3289. void GMainWindow::OnConfigureTas() {
  3290. ConfigureTasDialog dialog(this);
  3291. const auto result = dialog.exec();
  3292. if (result != QDialog::Accepted && !UISettings::values.configuration_applied) {
  3293. Settings::RestoreGlobalState(system->IsPoweredOn());
  3294. return;
  3295. } else if (result == QDialog::Accepted) {
  3296. dialog.ApplyConfiguration();
  3297. OnSaveConfig();
  3298. }
  3299. }
  3300. void GMainWindow::OnTasStartStop() {
  3301. if (!emulation_running) {
  3302. return;
  3303. }
  3304. // Disable system buttons to prevent TAS from executing a hotkey
  3305. auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
  3306. controller->ResetSystemButtons();
  3307. input_subsystem->GetTas()->StartStop();
  3308. OnTasStateChanged();
  3309. }
  3310. void GMainWindow::OnTasRecord() {
  3311. if (!emulation_running) {
  3312. return;
  3313. }
  3314. if (is_tas_recording_dialog_active) {
  3315. return;
  3316. }
  3317. // Disable system buttons to prevent TAS from recording a hotkey
  3318. auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
  3319. controller->ResetSystemButtons();
  3320. const bool is_recording = input_subsystem->GetTas()->Record();
  3321. if (!is_recording) {
  3322. is_tas_recording_dialog_active = true;
  3323. bool answer = question(this, tr("TAS Recording"), tr("Overwrite file of player 1?"),
  3324. QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
  3325. input_subsystem->GetTas()->SaveRecording(answer);
  3326. is_tas_recording_dialog_active = false;
  3327. }
  3328. OnTasStateChanged();
  3329. }
  3330. void GMainWindow::OnTasReset() {
  3331. input_subsystem->GetTas()->Reset();
  3332. }
  3333. void GMainWindow::OnToggleDockedMode() {
  3334. const bool is_docked = Settings::IsDockedMode();
  3335. auto* player_1 = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
  3336. auto* handheld = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
  3337. if (!is_docked && handheld->IsConnected()) {
  3338. QMessageBox::warning(this, tr("Invalid config detected"),
  3339. tr("Handheld controller can't be used on docked mode. Pro "
  3340. "controller will be selected."));
  3341. handheld->Disconnect();
  3342. player_1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
  3343. player_1->Connect();
  3344. controller_dialog->refreshConfiguration();
  3345. }
  3346. Settings::values.use_docked_mode.SetValue(is_docked ? Settings::ConsoleMode::Handheld
  3347. : Settings::ConsoleMode::Docked);
  3348. UpdateDockedButton();
  3349. OnDockedModeChanged(is_docked, !is_docked, *system);
  3350. }
  3351. void GMainWindow::OnToggleGpuAccuracy() {
  3352. switch (Settings::values.gpu_accuracy.GetValue()) {
  3353. case Settings::GpuAccuracy::High: {
  3354. Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::Normal);
  3355. break;
  3356. }
  3357. case Settings::GpuAccuracy::Normal:
  3358. case Settings::GpuAccuracy::Extreme:
  3359. default: {
  3360. Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::High);
  3361. break;
  3362. }
  3363. }
  3364. system->ApplySettings();
  3365. UpdateGPUAccuracyButton();
  3366. }
  3367. void GMainWindow::OnMute() {
  3368. Settings::values.audio_muted = !Settings::values.audio_muted;
  3369. UpdateVolumeUI();
  3370. }
  3371. void GMainWindow::OnDecreaseVolume() {
  3372. Settings::values.audio_muted = false;
  3373. const auto current_volume = static_cast<s32>(Settings::values.volume.GetValue());
  3374. int step = 5;
  3375. if (current_volume <= 30) {
  3376. step = 2;
  3377. }
  3378. if (current_volume <= 6) {
  3379. step = 1;
  3380. }
  3381. Settings::values.volume.SetValue(std::max(current_volume - step, 0));
  3382. UpdateVolumeUI();
  3383. }
  3384. void GMainWindow::OnIncreaseVolume() {
  3385. Settings::values.audio_muted = false;
  3386. const auto current_volume = static_cast<s32>(Settings::values.volume.GetValue());
  3387. int step = 5;
  3388. if (current_volume < 30) {
  3389. step = 2;
  3390. }
  3391. if (current_volume < 6) {
  3392. step = 1;
  3393. }
  3394. Settings::values.volume.SetValue(current_volume + step);
  3395. UpdateVolumeUI();
  3396. }
  3397. void GMainWindow::OnToggleAdaptingFilter() {
  3398. auto filter = Settings::values.scaling_filter.GetValue();
  3399. filter = static_cast<Settings::ScalingFilter>(static_cast<u32>(filter) + 1);
  3400. if (filter == Settings::ScalingFilter::MaxEnum) {
  3401. filter = Settings::ScalingFilter::NearestNeighbor;
  3402. }
  3403. Settings::values.scaling_filter.SetValue(filter);
  3404. filter_status_button->setChecked(true);
  3405. UpdateFilterText();
  3406. }
  3407. void GMainWindow::OnToggleGraphicsAPI() {
  3408. auto api = Settings::values.renderer_backend.GetValue();
  3409. if (api != Settings::RendererBackend::Vulkan) {
  3410. api = Settings::RendererBackend::Vulkan;
  3411. } else {
  3412. #ifdef HAS_OPENGL
  3413. api = Settings::RendererBackend::OpenGL;
  3414. #else
  3415. api = Settings::RendererBackend::Null;
  3416. #endif
  3417. }
  3418. Settings::values.renderer_backend.SetValue(api);
  3419. renderer_status_button->setChecked(api == Settings::RendererBackend::Vulkan);
  3420. UpdateAPIText();
  3421. }
  3422. void GMainWindow::OnConfigurePerGame() {
  3423. const u64 title_id = system->GetApplicationProcessProgramID();
  3424. OpenPerGameConfiguration(title_id, current_game_path.toStdString());
  3425. }
  3426. void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_name) {
  3427. const auto v_file = Core::GetGameFileFromPath(vfs, file_name);
  3428. Settings::SetConfiguringGlobal(false);
  3429. ConfigurePerGame dialog(this, title_id, file_name, vk_device_records, *system);
  3430. dialog.LoadFromFile(v_file);
  3431. const auto result = dialog.exec();
  3432. if (result != QDialog::Accepted && !UISettings::values.configuration_applied) {
  3433. Settings::RestoreGlobalState(system->IsPoweredOn());
  3434. return;
  3435. } else if (result == QDialog::Accepted) {
  3436. dialog.ApplyConfiguration();
  3437. }
  3438. const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
  3439. if (reload) {
  3440. game_list->PopulateAsync(UISettings::values.game_dirs);
  3441. }
  3442. // Do not cause the global config to write local settings into the config file
  3443. const bool is_powered_on = system->IsPoweredOn();
  3444. Settings::RestoreGlobalState(is_powered_on);
  3445. system->HIDCore().ReloadInputDevices();
  3446. UISettings::values.configuration_applied = false;
  3447. if (!is_powered_on) {
  3448. config->SaveAllValues();
  3449. }
  3450. }
  3451. void GMainWindow::OnLoadAmiibo() {
  3452. if (emu_thread == nullptr || !emu_thread->IsRunning()) {
  3453. return;
  3454. }
  3455. if (is_amiibo_file_select_active) {
  3456. return;
  3457. }
  3458. auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo();
  3459. // Remove amiibo if one is connected
  3460. if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) {
  3461. virtual_amiibo->CloseAmiibo();
  3462. QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed"));
  3463. return;
  3464. }
  3465. if (virtual_amiibo->GetCurrentState() != InputCommon::VirtualAmiibo::State::WaitingForAmiibo) {
  3466. QMessageBox::warning(this, tr("Error"), tr("The current game is not looking for amiibos"));
  3467. return;
  3468. }
  3469. is_amiibo_file_select_active = true;
  3470. const QString extensions{QStringLiteral("*.bin")};
  3471. const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions);
  3472. const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter);
  3473. is_amiibo_file_select_active = false;
  3474. if (filename.isEmpty()) {
  3475. return;
  3476. }
  3477. LoadAmiibo(filename);
  3478. }
  3479. bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text,
  3480. QMessageBox::StandardButtons buttons,
  3481. QMessageBox::StandardButton defaultButton) {
  3482. QMessageBox* box_dialog = new QMessageBox(parent);
  3483. box_dialog->setWindowTitle(title);
  3484. box_dialog->setText(text);
  3485. box_dialog->setStandardButtons(buttons);
  3486. box_dialog->setDefaultButton(defaultButton);
  3487. ControllerNavigation* controller_navigation =
  3488. new ControllerNavigation(system->HIDCore(), box_dialog);
  3489. connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
  3490. [box_dialog](Qt::Key key) {
  3491. QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
  3492. QCoreApplication::postEvent(box_dialog, event);
  3493. });
  3494. int res = box_dialog->exec();
  3495. controller_navigation->UnloadController();
  3496. return res == QMessageBox::Yes;
  3497. }
  3498. void GMainWindow::LoadAmiibo(const QString& filename) {
  3499. auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo();
  3500. const QString title = tr("Error loading Amiibo data");
  3501. // Remove amiibo if one is connected
  3502. if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) {
  3503. virtual_amiibo->CloseAmiibo();
  3504. QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed"));
  3505. return;
  3506. }
  3507. switch (virtual_amiibo->LoadAmiibo(filename.toStdString())) {
  3508. case InputCommon::VirtualAmiibo::Info::NotAnAmiibo:
  3509. QMessageBox::warning(this, title, tr("The selected file is not a valid amiibo"));
  3510. break;
  3511. case InputCommon::VirtualAmiibo::Info::UnableToLoad:
  3512. QMessageBox::warning(this, title, tr("The selected file is already on use"));
  3513. break;
  3514. case InputCommon::VirtualAmiibo::Info::WrongDeviceState:
  3515. QMessageBox::warning(this, title, tr("The current game is not looking for amiibos"));
  3516. break;
  3517. case InputCommon::VirtualAmiibo::Info::Unknown:
  3518. QMessageBox::warning(this, title, tr("An unknown error occurred"));
  3519. break;
  3520. default:
  3521. break;
  3522. }
  3523. }
  3524. void GMainWindow::OnOpenYuzuFolder() {
  3525. QDesktopServices::openUrl(QUrl::fromLocalFile(
  3526. QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::YuzuDir))));
  3527. }
  3528. void GMainWindow::OnVerifyInstalledContents() {
  3529. // Initialize a progress dialog.
  3530. QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
  3531. progress.setWindowModality(Qt::WindowModal);
  3532. progress.setMinimumDuration(100);
  3533. progress.setAutoClose(false);
  3534. progress.setAutoReset(false);
  3535. // Declare progress callback.
  3536. auto QtProgressCallback = [&](size_t total_size, size_t processed_size) {
  3537. progress.setValue(static_cast<int>((processed_size * 100) / total_size));
  3538. return progress.wasCanceled();
  3539. };
  3540. const std::vector<std::string> result =
  3541. ContentManager::VerifyInstalledContents(*system, *provider, QtProgressCallback);
  3542. progress.close();
  3543. if (result.empty()) {
  3544. QMessageBox::information(this, tr("Integrity verification succeeded!"),
  3545. tr("The operation completed successfully."));
  3546. } else {
  3547. const auto failed_names =
  3548. QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
  3549. QMessageBox::critical(
  3550. this, tr("Integrity verification failed!"),
  3551. tr("Verification failed for the following files:\n\n%1").arg(failed_names));
  3552. }
  3553. }
  3554. void GMainWindow::OnAbout() {
  3555. AboutDialog aboutDialog(this);
  3556. aboutDialog.exec();
  3557. }
  3558. void GMainWindow::OnToggleFilterBar() {
  3559. game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
  3560. if (ui->action_Show_Filter_Bar->isChecked()) {
  3561. game_list->SetFilterFocus();
  3562. } else {
  3563. game_list->ClearFilter();
  3564. }
  3565. }
  3566. void GMainWindow::OnToggleStatusBar() {
  3567. statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
  3568. }
  3569. void GMainWindow::OnAlbum() {
  3570. constexpr u64 AlbumId = static_cast<u64>(Service::AM::AppletProgramId::PhotoViewer);
  3571. auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
  3572. if (!bis_system) {
  3573. QMessageBox::warning(this, tr("No firmware available"),
  3574. tr("Please install the firmware to use the Album applet."));
  3575. return;
  3576. }
  3577. auto album_nca = bis_system->GetEntry(AlbumId, FileSys::ContentRecordType::Program);
  3578. if (!album_nca) {
  3579. QMessageBox::warning(this, tr("Album Applet"),
  3580. tr("Album applet is not available. Please reinstall firmware."));
  3581. return;
  3582. }
  3583. system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::PhotoViewer);
  3584. const auto filename = QString::fromStdString(album_nca->GetFullPath());
  3585. UISettings::values.roms_path = QFileInfo(filename).path().toStdString();
  3586. BootGame(filename, LibraryAppletParameters(AlbumId, Service::AM::AppletId::PhotoViewer));
  3587. }
  3588. void GMainWindow::OnCabinet(Service::NFP::CabinetMode mode) {
  3589. constexpr u64 CabinetId = static_cast<u64>(Service::AM::AppletProgramId::Cabinet);
  3590. auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
  3591. if (!bis_system) {
  3592. QMessageBox::warning(this, tr("No firmware available"),
  3593. tr("Please install the firmware to use the Cabinet applet."));
  3594. return;
  3595. }
  3596. auto cabinet_nca = bis_system->GetEntry(CabinetId, FileSys::ContentRecordType::Program);
  3597. if (!cabinet_nca) {
  3598. QMessageBox::warning(this, tr("Cabinet Applet"),
  3599. tr("Cabinet applet is not available. Please reinstall firmware."));
  3600. return;
  3601. }
  3602. system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Cabinet);
  3603. system->GetFrontendAppletHolder().SetCabinetMode(mode);
  3604. const auto filename = QString::fromStdString(cabinet_nca->GetFullPath());
  3605. UISettings::values.roms_path = QFileInfo(filename).path().toStdString();
  3606. BootGame(filename, LibraryAppletParameters(CabinetId, Service::AM::AppletId::Cabinet));
  3607. }
  3608. void GMainWindow::OnMiiEdit() {
  3609. constexpr u64 MiiEditId = static_cast<u64>(Service::AM::AppletProgramId::MiiEdit);
  3610. auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
  3611. if (!bis_system) {
  3612. QMessageBox::warning(this, tr("No firmware available"),
  3613. tr("Please install the firmware to use the Mii editor."));
  3614. return;
  3615. }
  3616. auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
  3617. if (!mii_applet_nca) {
  3618. QMessageBox::warning(this, tr("Mii Edit Applet"),
  3619. tr("Mii editor is not available. Please reinstall firmware."));
  3620. return;
  3621. }
  3622. system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::MiiEdit);
  3623. const auto filename = QString::fromStdString((mii_applet_nca->GetFullPath()));
  3624. UISettings::values.roms_path = QFileInfo(filename).path().toStdString();
  3625. BootGame(filename, LibraryAppletParameters(MiiEditId, Service::AM::AppletId::MiiEdit));
  3626. }
  3627. void GMainWindow::OnOpenControllerMenu() {
  3628. constexpr u64 ControllerAppletId = static_cast<u64>(Service::AM::AppletProgramId::Controller);
  3629. auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
  3630. if (!bis_system) {
  3631. QMessageBox::warning(this, tr("No firmware available"),
  3632. tr("Please install the firmware to use the Controller Menu."));
  3633. return;
  3634. }
  3635. auto controller_applet_nca =
  3636. bis_system->GetEntry(ControllerAppletId, FileSys::ContentRecordType::Program);
  3637. if (!controller_applet_nca) {
  3638. QMessageBox::warning(this, tr("Controller Applet"),
  3639. tr("Controller Menu is not available. Please reinstall firmware."));
  3640. return;
  3641. }
  3642. system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Controller);
  3643. const auto filename = QString::fromStdString((controller_applet_nca->GetFullPath()));
  3644. UISettings::values.roms_path = QFileInfo(filename).path().toStdString();
  3645. BootGame(filename,
  3646. LibraryAppletParameters(ControllerAppletId, Service::AM::AppletId::Controller));
  3647. }
  3648. void GMainWindow::OnCaptureScreenshot() {
  3649. if (emu_thread == nullptr || !emu_thread->IsRunning()) {
  3650. return;
  3651. }
  3652. const u64 title_id = system->GetApplicationProcessProgramID();
  3653. const auto screenshot_path =
  3654. QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ScreenshotsDir));
  3655. const auto date =
  3656. QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz"));
  3657. QString filename = QStringLiteral("%1/%2_%3.png")
  3658. .arg(screenshot_path)
  3659. .arg(title_id, 16, 16, QLatin1Char{'0'})
  3660. .arg(date);
  3661. if (!Common::FS::CreateDir(screenshot_path.toStdString())) {
  3662. return;
  3663. }
  3664. #ifdef _WIN32
  3665. if (UISettings::values.enable_screenshot_save_as) {
  3666. OnPauseGame();
  3667. filename = QFileDialog::getSaveFileName(this, tr("Capture Screenshot"), filename,
  3668. tr("PNG Image (*.png)"));
  3669. OnStartGame();
  3670. if (filename.isEmpty()) {
  3671. return;
  3672. }
  3673. }
  3674. #endif
  3675. render_window->CaptureScreenshot(filename);
  3676. }
  3677. // TODO: Written 2020-10-01: Remove per-game config migration code when it is irrelevant
  3678. void GMainWindow::MigrateConfigFiles() {
  3679. const auto config_dir_fs_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir);
  3680. const QDir config_dir =
  3681. QDir(QString::fromStdString(Common::FS::PathToUTF8String(config_dir_fs_path)));
  3682. const QStringList config_dir_list = config_dir.entryList(QStringList(QStringLiteral("*.ini")));
  3683. if (!Common::FS::CreateDirs(config_dir_fs_path / "custom")) {
  3684. LOG_ERROR(Frontend, "Failed to create new config file directory");
  3685. }
  3686. for (auto it = config_dir_list.constBegin(); it != config_dir_list.constEnd(); ++it) {
  3687. const auto filename = it->toStdString();
  3688. if (filename.find_first_not_of("0123456789abcdefACBDEF", 0) < 16) {
  3689. continue;
  3690. }
  3691. const auto origin = config_dir_fs_path / filename;
  3692. const auto destination = config_dir_fs_path / "custom" / filename;
  3693. LOG_INFO(Frontend, "Migrating config file from {} to {}", origin.string(),
  3694. destination.string());
  3695. if (!Common::FS::RenameFile(origin, destination)) {
  3696. // Delete the old config file if one already exists in the new location.
  3697. Common::FS::RemoveFile(origin);
  3698. }
  3699. }
  3700. }
  3701. void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_view title_version,
  3702. std::string_view gpu_vendor) {
  3703. const auto branch_name = std::string(Common::g_scm_branch);
  3704. const auto description = std::string(Common::g_scm_desc);
  3705. const auto build_id = std::string(Common::g_build_id);
  3706. const auto yuzu_title = fmt::format("yuzu | {}-{}", branch_name, description);
  3707. const auto override_title =
  3708. fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id);
  3709. const auto window_title = override_title.empty() ? yuzu_title : override_title;
  3710. if (title_name.empty()) {
  3711. setWindowTitle(QString::fromStdString(window_title));
  3712. } else {
  3713. const auto run_title = [window_title, title_name, title_version, gpu_vendor]() {
  3714. if (title_version.empty()) {
  3715. return fmt::format("{} | {} | {}", window_title, title_name, gpu_vendor);
  3716. }
  3717. return fmt::format("{} | {} | {} | {}", window_title, title_name, title_version,
  3718. gpu_vendor);
  3719. }();
  3720. setWindowTitle(QString::fromStdString(run_title));
  3721. }
  3722. }
  3723. std::string GMainWindow::CreateTASFramesString(
  3724. std::array<size_t, InputCommon::TasInput::PLAYER_NUMBER> frames) const {
  3725. std::string string = "";
  3726. size_t maxPlayerIndex = 0;
  3727. for (size_t i = 0; i < frames.size(); i++) {
  3728. if (frames[i] != 0) {
  3729. if (maxPlayerIndex != 0)
  3730. string += ", ";
  3731. while (maxPlayerIndex++ != i)
  3732. string += "0, ";
  3733. string += std::to_string(frames[i]);
  3734. }
  3735. }
  3736. return string;
  3737. }
  3738. QString GMainWindow::GetTasStateDescription() const {
  3739. auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus();
  3740. std::string tas_frames_string = CreateTASFramesString(total_tas_frames);
  3741. switch (tas_status) {
  3742. case InputCommon::TasInput::TasState::Running:
  3743. return tr("TAS state: Running %1/%2")
  3744. .arg(current_tas_frame)
  3745. .arg(QString::fromStdString(tas_frames_string));
  3746. case InputCommon::TasInput::TasState::Recording:
  3747. return tr("TAS state: Recording %1").arg(total_tas_frames[0]);
  3748. case InputCommon::TasInput::TasState::Stopped:
  3749. return tr("TAS state: Idle %1/%2")
  3750. .arg(current_tas_frame)
  3751. .arg(QString::fromStdString(tas_frames_string));
  3752. default:
  3753. return tr("TAS State: Invalid");
  3754. }
  3755. }
  3756. void GMainWindow::OnTasStateChanged() {
  3757. bool is_running = false;
  3758. bool is_recording = false;
  3759. if (emulation_running) {
  3760. const InputCommon::TasInput::TasState tas_status =
  3761. std::get<0>(input_subsystem->GetTas()->GetStatus());
  3762. is_running = tas_status == InputCommon::TasInput::TasState::Running;
  3763. is_recording = tas_status == InputCommon::TasInput::TasState::Recording;
  3764. }
  3765. ui->action_TAS_Start->setText(is_running ? tr("&Stop Running") : tr("&Start"));
  3766. ui->action_TAS_Record->setText(is_recording ? tr("Stop R&ecording") : tr("R&ecord"));
  3767. ui->action_TAS_Start->setEnabled(emulation_running);
  3768. ui->action_TAS_Record->setEnabled(emulation_running);
  3769. ui->action_TAS_Reset->setEnabled(emulation_running);
  3770. }
  3771. void GMainWindow::UpdateStatusBar() {
  3772. if (emu_thread == nullptr || !system->IsPoweredOn()) {
  3773. status_bar_update_timer.stop();
  3774. return;
  3775. }
  3776. if (Settings::values.tas_enable) {
  3777. tas_label->setText(GetTasStateDescription());
  3778. } else {
  3779. tas_label->clear();
  3780. }
  3781. auto results = system->GetAndResetPerfStats();
  3782. auto& shader_notify = system->GPU().ShaderNotify();
  3783. const int shaders_building = shader_notify.ShadersBuilding();
  3784. if (shaders_building > 0) {
  3785. shader_building_label->setText(tr("Building: %n shader(s)", "", shaders_building));
  3786. shader_building_label->setVisible(true);
  3787. } else {
  3788. shader_building_label->setVisible(false);
  3789. }
  3790. const auto res_info = Settings::values.resolution_info;
  3791. const auto res_scale = res_info.up_factor;
  3792. res_scale_label->setText(
  3793. tr("Scale: %1x", "%1 is the resolution scaling factor").arg(res_scale));
  3794. if (Settings::values.use_speed_limit.GetValue()) {
  3795. emu_speed_label->setText(tr("Speed: %1% / %2%")
  3796. .arg(results.emulation_speed * 100.0, 0, 'f', 0)
  3797. .arg(Settings::values.speed_limit.GetValue()));
  3798. } else {
  3799. emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
  3800. }
  3801. if (!Settings::values.use_speed_limit) {
  3802. game_fps_label->setText(
  3803. tr("Game: %1 FPS (Unlocked)").arg(std::round(results.average_game_fps), 0, 'f', 0));
  3804. } else {
  3805. game_fps_label->setText(
  3806. tr("Game: %1 FPS").arg(std::round(results.average_game_fps), 0, 'f', 0));
  3807. }
  3808. emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
  3809. res_scale_label->setVisible(true);
  3810. emu_speed_label->setVisible(!Settings::values.use_multi_core.GetValue());
  3811. game_fps_label->setVisible(true);
  3812. emu_frametime_label->setVisible(true);
  3813. firmware_label->setVisible(false);
  3814. }
  3815. void GMainWindow::UpdateGPUAccuracyButton() {
  3816. const auto gpu_accuracy = Settings::values.gpu_accuracy.GetValue();
  3817. const auto gpu_accuracy_text =
  3818. ConfigurationShared::gpu_accuracy_texts_map.find(gpu_accuracy)->second;
  3819. gpu_accuracy_button->setText(gpu_accuracy_text.toUpper());
  3820. gpu_accuracy_button->setChecked(gpu_accuracy != Settings::GpuAccuracy::Normal);
  3821. }
  3822. void GMainWindow::UpdateDockedButton() {
  3823. const auto console_mode = Settings::values.use_docked_mode.GetValue();
  3824. dock_status_button->setChecked(Settings::IsDockedMode());
  3825. dock_status_button->setText(
  3826. ConfigurationShared::use_docked_mode_texts_map.find(console_mode)->second.toUpper());
  3827. }
  3828. void GMainWindow::UpdateAPIText() {
  3829. const auto api = Settings::values.renderer_backend.GetValue();
  3830. const auto renderer_status_text =
  3831. ConfigurationShared::renderer_backend_texts_map.find(api)->second;
  3832. renderer_status_button->setText(
  3833. api == Settings::RendererBackend::OpenGL
  3834. ? tr("%1 %2").arg(renderer_status_text.toUpper(),
  3835. ConfigurationShared::shader_backend_texts_map
  3836. .find(Settings::values.shader_backend.GetValue())
  3837. ->second)
  3838. : renderer_status_text.toUpper());
  3839. }
  3840. void GMainWindow::UpdateFilterText() {
  3841. const auto filter = Settings::values.scaling_filter.GetValue();
  3842. const auto filter_text = ConfigurationShared::scaling_filter_texts_map.find(filter)->second;
  3843. filter_status_button->setText(filter == Settings::ScalingFilter::Fsr ? tr("FSR")
  3844. : filter_text.toUpper());
  3845. }
  3846. void GMainWindow::UpdateAAText() {
  3847. const auto aa_mode = Settings::values.anti_aliasing.GetValue();
  3848. const auto aa_text = ConfigurationShared::anti_aliasing_texts_map.find(aa_mode)->second;
  3849. aa_status_button->setText(aa_mode == Settings::AntiAliasing::None
  3850. ? QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "NO AA"))
  3851. : aa_text.toUpper());
  3852. }
  3853. void GMainWindow::UpdateVolumeUI() {
  3854. const auto volume_value = static_cast<int>(Settings::values.volume.GetValue());
  3855. volume_slider->setValue(volume_value);
  3856. if (Settings::values.audio_muted) {
  3857. volume_button->setChecked(false);
  3858. volume_button->setText(tr("VOLUME: MUTE"));
  3859. } else {
  3860. volume_button->setChecked(true);
  3861. volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value));
  3862. }
  3863. }
  3864. void GMainWindow::UpdateStatusButtons() {
  3865. renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() ==
  3866. Settings::RendererBackend::Vulkan);
  3867. UpdateAPIText();
  3868. UpdateGPUAccuracyButton();
  3869. UpdateDockedButton();
  3870. UpdateFilterText();
  3871. UpdateAAText();
  3872. UpdateVolumeUI();
  3873. }
  3874. void GMainWindow::UpdateUISettings() {
  3875. if (!ui->action_Fullscreen->isChecked()) {
  3876. UISettings::values.geometry = saveGeometry();
  3877. UISettings::values.renderwindow_geometry = render_window->saveGeometry();
  3878. }
  3879. UISettings::values.state = saveState();
  3880. #if MICROPROFILE_ENABLED
  3881. UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
  3882. UISettings::values.microprofile_visible = microProfileDialog->isVisible();
  3883. #endif
  3884. UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked();
  3885. UISettings::values.fullscreen = ui->action_Fullscreen->isChecked();
  3886. UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked();
  3887. UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked();
  3888. UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked();
  3889. UISettings::values.first_start = false;
  3890. }
  3891. void GMainWindow::UpdateInputDrivers() {
  3892. if (!input_subsystem) {
  3893. return;
  3894. }
  3895. input_subsystem->PumpEvents();
  3896. }
  3897. void GMainWindow::HideMouseCursor() {
  3898. if (emu_thread == nullptr && UISettings::values.hide_mouse) {
  3899. mouse_hide_timer.stop();
  3900. ShowMouseCursor();
  3901. return;
  3902. }
  3903. render_window->setCursor(QCursor(Qt::BlankCursor));
  3904. }
  3905. void GMainWindow::ShowMouseCursor() {
  3906. render_window->unsetCursor();
  3907. if (emu_thread != nullptr && UISettings::values.hide_mouse) {
  3908. mouse_hide_timer.start();
  3909. }
  3910. }
  3911. void GMainWindow::OnMouseActivity() {
  3912. if (!Settings::values.mouse_panning) {
  3913. ShowMouseCursor();
  3914. }
  3915. }
  3916. void GMainWindow::OnCheckFirmwareDecryption() {
  3917. system->GetFileSystemController().CreateFactories(*vfs);
  3918. if (!ContentManager::AreKeysPresent()) {
  3919. QMessageBox::warning(
  3920. this, tr("Derivation Components Missing"),
  3921. tr("Encryption keys are missing. "
  3922. "<br>Please follow <a href='https://yuzu-emu.org/help/quickstart/'>the yuzu "
  3923. "quickstart guide</a> to get all your keys, firmware and "
  3924. "games."));
  3925. }
  3926. SetFirmwareVersion();
  3927. UpdateMenuState();
  3928. }
  3929. bool GMainWindow::CheckFirmwarePresence() {
  3930. constexpr u64 MiiEditId = static_cast<u64>(Service::AM::AppletProgramId::MiiEdit);
  3931. auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
  3932. if (!bis_system) {
  3933. return false;
  3934. }
  3935. auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
  3936. if (!mii_applet_nca) {
  3937. return false;
  3938. }
  3939. return true;
  3940. }
  3941. void GMainWindow::SetFirmwareVersion() {
  3942. Service::Set::FirmwareVersionFormat firmware_data{};
  3943. const auto result = Service::Set::GetFirmwareVersionImpl(
  3944. firmware_data, *system, Service::Set::GetFirmwareVersionType::Version2);
  3945. if (result.IsError() || !CheckFirmwarePresence()) {
  3946. LOG_INFO(Frontend, "Installed firmware: No firmware available");
  3947. firmware_label->setVisible(false);
  3948. return;
  3949. }
  3950. firmware_label->setVisible(true);
  3951. const std::string display_version(firmware_data.display_version.data());
  3952. const std::string display_title(firmware_data.display_title.data());
  3953. LOG_INFO(Frontend, "Installed firmware: {}", display_title);
  3954. firmware_label->setText(QString::fromStdString(display_version));
  3955. firmware_label->setToolTip(QString::fromStdString(display_title));
  3956. }
  3957. bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id,
  3958. u64* selected_title_id, u8* selected_content_record_type) {
  3959. using ContentInfo = std::tuple<u64, FileSys::TitleType, FileSys::ContentRecordType>;
  3960. boost::container::flat_set<ContentInfo> available_title_ids;
  3961. const auto RetrieveEntries = [&](FileSys::TitleType title_type,
  3962. FileSys::ContentRecordType record_type) {
  3963. const auto entries = installed.ListEntriesFilter(title_type, record_type);
  3964. for (const auto& entry : entries) {
  3965. if (FileSys::GetBaseTitleID(entry.title_id) == program_id &&
  3966. installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) {
  3967. available_title_ids.insert({entry.title_id, title_type, record_type});
  3968. }
  3969. }
  3970. };
  3971. RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program);
  3972. RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument);
  3973. RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation);
  3974. RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
  3975. if (available_title_ids.empty()) {
  3976. return false;
  3977. }
  3978. size_t title_index = 0;
  3979. if (available_title_ids.size() > 1) {
  3980. QStringList list;
  3981. for (auto& [title_id, title_type, record_type] : available_title_ids) {
  3982. const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id));
  3983. if (record_type == FileSys::ContentRecordType::Program) {
  3984. list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id));
  3985. } else if (record_type == FileSys::ContentRecordType::HtmlDocument) {
  3986. list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id));
  3987. } else if (record_type == FileSys::ContentRecordType::LegalInformation) {
  3988. list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id));
  3989. } else {
  3990. list.push_back(
  3991. QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id));
  3992. }
  3993. }
  3994. bool ok;
  3995. const auto res = QInputDialog::getItem(
  3996. this, tr("Select RomFS Dump Target"),
  3997. tr("Please select which RomFS you would like to dump."), list, 0, false, &ok);
  3998. if (!ok) {
  3999. return false;
  4000. }
  4001. title_index = list.indexOf(res);
  4002. }
  4003. const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index);
  4004. *selected_title_id = title_id;
  4005. *selected_content_record_type = static_cast<u8>(record_type);
  4006. return true;
  4007. }
  4008. bool GMainWindow::ConfirmClose() {
  4009. if (emu_thread == nullptr ||
  4010. UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Never) {
  4011. return true;
  4012. }
  4013. if (!system->GetExitLocked() &&
  4014. UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Based_On_Game) {
  4015. return true;
  4016. }
  4017. const auto text = tr("Are you sure you want to close yuzu?");
  4018. return question(this, tr("yuzu"), text);
  4019. }
  4020. void GMainWindow::closeEvent(QCloseEvent* event) {
  4021. if (!ConfirmClose()) {
  4022. event->ignore();
  4023. return;
  4024. }
  4025. UpdateUISettings();
  4026. game_list->SaveInterfaceLayout();
  4027. UISettings::SaveWindowState();
  4028. hotkey_registry.SaveHotkeys();
  4029. // Unload controllers early
  4030. controller_dialog->UnloadController();
  4031. game_list->UnloadController();
  4032. // Shutdown session if the emu thread is active...
  4033. if (emu_thread != nullptr) {
  4034. ShutdownGame();
  4035. }
  4036. render_window->close();
  4037. multiplayer_state->Close();
  4038. system->HIDCore().UnloadInputDevices();
  4039. system->GetRoomNetwork().Shutdown();
  4040. QWidget::closeEvent(event);
  4041. }
  4042. static bool IsSingleFileDropEvent(const QMimeData* mime) {
  4043. return mime->hasUrls() && mime->urls().length() == 1;
  4044. }
  4045. void GMainWindow::AcceptDropEvent(QDropEvent* event) {
  4046. if (IsSingleFileDropEvent(event->mimeData())) {
  4047. event->setDropAction(Qt::DropAction::LinkAction);
  4048. event->accept();
  4049. }
  4050. }
  4051. bool GMainWindow::DropAction(QDropEvent* event) {
  4052. if (!IsSingleFileDropEvent(event->mimeData())) {
  4053. return false;
  4054. }
  4055. const QMimeData* mime_data = event->mimeData();
  4056. const QString& filename = mime_data->urls().at(0).toLocalFile();
  4057. if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) {
  4058. // Amiibo
  4059. LoadAmiibo(filename);
  4060. } else {
  4061. // Game
  4062. if (ConfirmChangeGame()) {
  4063. BootGame(filename, ApplicationAppletParameters());
  4064. }
  4065. }
  4066. return true;
  4067. }
  4068. void GMainWindow::dropEvent(QDropEvent* event) {
  4069. DropAction(event);
  4070. }
  4071. void GMainWindow::dragEnterEvent(QDragEnterEvent* event) {
  4072. AcceptDropEvent(event);
  4073. }
  4074. void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
  4075. AcceptDropEvent(event);
  4076. }
  4077. bool GMainWindow::ConfirmChangeGame() {
  4078. if (emu_thread == nullptr)
  4079. return true;
  4080. // Use custom question to link controller navigation
  4081. return question(
  4082. this, tr("yuzu"),
  4083. tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."),
  4084. QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
  4085. }
  4086. bool GMainWindow::ConfirmForceLockedExit() {
  4087. if (emu_thread == nullptr) {
  4088. return true;
  4089. }
  4090. const auto text = tr("The currently running application has requested yuzu to not exit.\n\n"
  4091. "Would you like to bypass this and exit anyway?");
  4092. return question(this, tr("yuzu"), text);
  4093. }
  4094. void GMainWindow::RequestGameExit() {
  4095. if (!system->IsPoweredOn()) {
  4096. return;
  4097. }
  4098. system->SetExitRequested(true);
  4099. system->GetAppletManager().RequestExit();
  4100. }
  4101. void GMainWindow::RequestGameResume() {
  4102. system->GetAppletManager().RequestResume();
  4103. }
  4104. void GMainWindow::filterBarSetChecked(bool state) {
  4105. ui->action_Show_Filter_Bar->setChecked(state);
  4106. emit(OnToggleFilterBar());
  4107. }
  4108. static void AdjustLinkColor() {
  4109. QPalette new_pal(qApp->palette());
  4110. if (UISettings::IsDarkTheme()) {
  4111. new_pal.setColor(QPalette::Link, QColor(0, 190, 255, 255));
  4112. } else {
  4113. new_pal.setColor(QPalette::Link, QColor(0, 140, 200, 255));
  4114. }
  4115. if (qApp->palette().color(QPalette::Link) != new_pal.color(QPalette::Link)) {
  4116. qApp->setPalette(new_pal);
  4117. }
  4118. }
  4119. void GMainWindow::UpdateUITheme() {
  4120. const QString default_theme = QString::fromUtf8(
  4121. UISettings::themes[static_cast<size_t>(UISettings::default_theme)].second);
  4122. QString current_theme = QString::fromStdString(UISettings::values.theme);
  4123. if (current_theme.isEmpty()) {
  4124. current_theme = default_theme;
  4125. }
  4126. #ifdef _WIN32
  4127. QIcon::setThemeName(current_theme);
  4128. AdjustLinkColor();
  4129. #else
  4130. if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) {
  4131. QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme
  4132. : startup_icon_theme);
  4133. QIcon::setThemeSearchPaths(QStringList(default_theme_paths));
  4134. if (CheckDarkMode()) {
  4135. current_theme = QStringLiteral("default_dark");
  4136. }
  4137. } else {
  4138. QIcon::setThemeName(current_theme);
  4139. QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons")));
  4140. AdjustLinkColor();
  4141. }
  4142. #endif
  4143. if (current_theme != default_theme) {
  4144. QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)};
  4145. QFile f(theme_uri);
  4146. if (!f.open(QFile::ReadOnly | QFile::Text)) {
  4147. LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme",
  4148. UISettings::values.theme);
  4149. current_theme = default_theme;
  4150. }
  4151. }
  4152. QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)};
  4153. QFile f(theme_uri);
  4154. if (f.open(QFile::ReadOnly | QFile::Text)) {
  4155. QTextStream ts(&f);
  4156. qApp->setStyleSheet(ts.readAll());
  4157. setStyleSheet(ts.readAll());
  4158. } else {
  4159. LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found",
  4160. UISettings::values.theme);
  4161. qApp->setStyleSheet({});
  4162. setStyleSheet({});
  4163. }
  4164. }
  4165. void GMainWindow::LoadTranslation() {
  4166. bool loaded;
  4167. if (UISettings::values.language.GetValue().empty()) {
  4168. // If the selected language is empty, use system locale
  4169. loaded = translator.load(QLocale(), {}, {}, QStringLiteral(":/languages/"));
  4170. } else {
  4171. // Otherwise load from the specified file
  4172. loaded = translator.load(QString::fromStdString(UISettings::values.language.GetValue()),
  4173. QStringLiteral(":/languages/"));
  4174. }
  4175. if (loaded) {
  4176. qApp->installTranslator(&translator);
  4177. } else {
  4178. UISettings::values.language = std::string("en");
  4179. }
  4180. }
  4181. void GMainWindow::OnLanguageChanged(const QString& locale) {
  4182. if (UISettings::values.language.GetValue() != std::string("en")) {
  4183. qApp->removeTranslator(&translator);
  4184. }
  4185. UISettings::values.language = locale.toStdString();
  4186. LoadTranslation();
  4187. ui->retranslateUi(this);
  4188. multiplayer_state->retranslateUi();
  4189. UpdateWindowTitle();
  4190. }
  4191. void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
  4192. #ifdef USE_DISCORD_PRESENCE
  4193. if (state) {
  4194. discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>(*system);
  4195. } else {
  4196. discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
  4197. }
  4198. #else
  4199. discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
  4200. #endif
  4201. discord_rpc->Update();
  4202. }
  4203. #ifdef __unix__
  4204. void GMainWindow::SetGamemodeEnabled(bool state) {
  4205. if (emulation_running) {
  4206. Common::Linux::SetGamemodeState(state);
  4207. }
  4208. }
  4209. #endif
  4210. void GMainWindow::changeEvent(QEvent* event) {
  4211. #ifdef __unix__
  4212. // PaletteChange event appears to only reach so far into the GUI, explicitly asking to
  4213. // UpdateUITheme is a decent work around
  4214. if (event->type() == QEvent::PaletteChange) {
  4215. const QPalette test_palette(qApp->palette());
  4216. const QString current_theme = QString::fromStdString(UISettings::values.theme);
  4217. // Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too
  4218. static QColor last_window_color;
  4219. const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window);
  4220. if (last_window_color != window_color && (current_theme == QStringLiteral("default") ||
  4221. current_theme == QStringLiteral("colorful"))) {
  4222. UpdateUITheme();
  4223. }
  4224. last_window_color = window_color;
  4225. }
  4226. #endif // __unix__
  4227. QWidget::changeEvent(event);
  4228. }
  4229. Service::AM::FrontendAppletParameters GMainWindow::ApplicationAppletParameters() {
  4230. return Service::AM::FrontendAppletParameters{
  4231. .applet_id = Service::AM::AppletId::Application,
  4232. .applet_type = Service::AM::AppletType::Application,
  4233. };
  4234. }
  4235. Service::AM::FrontendAppletParameters GMainWindow::LibraryAppletParameters(
  4236. u64 program_id, Service::AM::AppletId applet_id) {
  4237. return Service::AM::FrontendAppletParameters{
  4238. .program_id = program_id,
  4239. .applet_id = applet_id,
  4240. .applet_type = Service::AM::AppletType::LibraryApplet,
  4241. };
  4242. }
  4243. void VolumeButton::wheelEvent(QWheelEvent* event) {
  4244. int num_degrees = event->angleDelta().y() / 8;
  4245. int num_steps = (num_degrees / 15) * scroll_multiplier;
  4246. // Stated in QT docs: Most mouse types work in steps of 15 degrees, in which case the delta
  4247. // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees.
  4248. if (num_steps > 0) {
  4249. Settings::values.volume.SetValue(
  4250. std::min(200, Settings::values.volume.GetValue() + num_steps));
  4251. } else {
  4252. Settings::values.volume.SetValue(
  4253. std::max(0, Settings::values.volume.GetValue() + num_steps));
  4254. }
  4255. scroll_multiplier = std::min(MaxMultiplier, scroll_multiplier * 2);
  4256. scroll_timer.start(100); // reset the multiplier if no scroll event occurs within 100 ms
  4257. emit VolumeChanged();
  4258. event->accept();
  4259. }
  4260. void VolumeButton::ResetMultiplier() {
  4261. scroll_multiplier = 1;
  4262. }
  4263. #ifdef main
  4264. #undef main
  4265. #endif
  4266. static void SetHighDPIAttributes() {
  4267. #ifdef _WIN32
  4268. // For Windows, we want to avoid scaling artifacts on fractional scaling ratios.
  4269. // This is done by setting the optimal scaling policy for the primary screen.
  4270. // Create a temporary QApplication.
  4271. int temp_argc = 0;
  4272. char** temp_argv = nullptr;
  4273. QApplication temp{temp_argc, temp_argv};
  4274. // Get the current screen geometry.
  4275. const QScreen* primary_screen = QGuiApplication::primaryScreen();
  4276. if (primary_screen == nullptr) {
  4277. return;
  4278. }
  4279. const QRect screen_rect = primary_screen->geometry();
  4280. const int real_width = screen_rect.width();
  4281. const int real_height = screen_rect.height();
  4282. const float real_ratio = primary_screen->logicalDotsPerInch() / 96.0f;
  4283. // Recommended minimum width and height for proper window fit.
  4284. // Any screen with a lower resolution than this will still have a scale of 1.
  4285. constexpr float minimum_width = 1350.0f;
  4286. constexpr float minimum_height = 900.0f;
  4287. const float width_ratio = std::max(1.0f, real_width / minimum_width);
  4288. const float height_ratio = std::max(1.0f, real_height / minimum_height);
  4289. // Get the lower of the 2 ratios and truncate, this is the maximum integer scale.
  4290. const float max_ratio = std::trunc(std::min(width_ratio, height_ratio));
  4291. if (max_ratio > real_ratio) {
  4292. QApplication::setHighDpiScaleFactorRoundingPolicy(
  4293. Qt::HighDpiScaleFactorRoundingPolicy::Round);
  4294. } else {
  4295. QApplication::setHighDpiScaleFactorRoundingPolicy(
  4296. Qt::HighDpiScaleFactorRoundingPolicy::Floor);
  4297. }
  4298. #else
  4299. // Other OSes should be better than Windows at fractional scaling.
  4300. QApplication::setHighDpiScaleFactorRoundingPolicy(
  4301. Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
  4302. #endif
  4303. QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
  4304. QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
  4305. }
  4306. int main(int argc, char* argv[]) {
  4307. std::unique_ptr<QtConfig> config = std::make_unique<QtConfig>();
  4308. UISettings::RestoreWindowState(config);
  4309. bool has_broken_vulkan = false;
  4310. bool is_child = false;
  4311. if (CheckEnvVars(&is_child)) {
  4312. return 0;
  4313. }
  4314. if (StartupChecks(argv[0], &has_broken_vulkan,
  4315. Settings::values.perform_vulkan_check.GetValue())) {
  4316. return 0;
  4317. }
  4318. #ifdef YUZU_CRASH_DUMPS
  4319. Breakpad::InstallCrashHandler();
  4320. #endif
  4321. Common::DetachedTasks detached_tasks;
  4322. MicroProfileOnThreadCreate("Frontend");
  4323. SCOPE_EXIT({ MicroProfileShutdown(); });
  4324. Common::ConfigureNvidiaEnvironmentFlags();
  4325. // Init settings params
  4326. QCoreApplication::setOrganizationName(QStringLiteral("yuzu team"));
  4327. QCoreApplication::setApplicationName(QStringLiteral("yuzu"));
  4328. #ifdef _WIN32
  4329. // Increases the maximum open file limit to 8192
  4330. _setmaxstdio(8192);
  4331. #endif
  4332. #ifdef __APPLE__
  4333. // If you start a bundle (binary) on OSX without the Terminal, the working directory is "/".
  4334. // But since we require the working directory to be the executable path for the location of
  4335. // the user folder in the Qt Frontend, we need to cd into that working directory
  4336. const auto bin_path = Common::FS::GetBundleDirectory() / "..";
  4337. chdir(Common::FS::PathToUTF8String(bin_path).c_str());
  4338. #endif
  4339. #ifdef __linux__
  4340. // Set the DISPLAY variable in order to open web browsers
  4341. // TODO (lat9nq): Find a better solution for AppImages to start external applications
  4342. if (QString::fromLocal8Bit(qgetenv("DISPLAY")).isEmpty()) {
  4343. qputenv("DISPLAY", ":0");
  4344. }
  4345. // Fix the Wayland appId. This needs to match the name of the .desktop file without the .desktop
  4346. // suffix.
  4347. QGuiApplication::setDesktopFileName(QStringLiteral("org.yuzu_emu.yuzu"));
  4348. #endif
  4349. SetHighDPIAttributes();
  4350. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  4351. // Disables the "?" button on all dialogs. Disabled by default on Qt6.
  4352. QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton);
  4353. #endif
  4354. // Enables the core to make the qt created contexts current on std::threads
  4355. QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
  4356. QApplication app(argc, argv);
  4357. #ifdef _WIN32
  4358. OverrideWindowsFont();
  4359. #endif
  4360. // Workaround for QTBUG-85409, for Suzhou numerals the number 1 is actually \u3021
  4361. // so we can see if we get \u3008 instead
  4362. // TL;DR all other number formats are consecutive in unicode code points
  4363. // This bug is fixed in Qt6, specifically 6.0.0-alpha1
  4364. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  4365. const QLocale locale = QLocale::system();
  4366. if (QStringLiteral("\u3008") == locale.toString(1)) {
  4367. QLocale::setDefault(QLocale::system().name());
  4368. }
  4369. #endif
  4370. // Qt changes the locale and causes issues in float conversion using std::to_string() when
  4371. // generating shaders
  4372. setlocale(LC_ALL, "C");
  4373. GMainWindow main_window{std::move(config), has_broken_vulkan};
  4374. // After settings have been loaded by GMainWindow, apply the filter
  4375. main_window.show();
  4376. QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
  4377. &GMainWindow::OnAppFocusStateChanged);
  4378. int result = app.exec();
  4379. detached_tasks.WaitForAllTasks();
  4380. return result;
  4381. }