1 #include "Core/Analytics.h"
2 
3 #include <array>
4 #include <memory>
5 #include <mutex>
6 #include <string>
7 #include <vector>
8 
9 #include <fmt/format.h>
10 #include <mbedtls/sha1.h>
11 
12 #if defined(_WIN32)
13 #include <windows.h>
14 #elif defined(__APPLE__)
15 #include <objc/message.h>
16 #elif defined(ANDROID)
17 #include <functional>
18 #include "Common/AndroidAnalytics.h"
19 #endif
20 
21 #include "Common/Analytics.h"
22 #include "Common/CPUDetect.h"
23 #include "Common/CommonTypes.h"
24 #include "Common/Config/Config.h"
25 #include "Common/Random.h"
26 #include "Common/Timer.h"
27 #include "Common/Version.h"
28 #include "Core/Config/MainSettings.h"
29 #include "Core/ConfigManager.h"
30 #include "Core/HW/GCPad.h"
31 #include "Core/Movie.h"
32 #include "Core/NetPlayProto.h"
33 #include "InputCommon/GCAdapter.h"
34 #include "InputCommon/InputConfig.h"
35 #include "VideoCommon/VideoBackendBase.h"
36 #include "VideoCommon/VideoConfig.h"
37 
38 namespace
39 {
40 constexpr char ANALYTICS_ENDPOINT[] = "https://analytics.dolphin-emu.org/report";
41 }  // namespace
42 
43 #if defined(ANDROID)
44 static std::function<std::string(std::string)> s_get_val_func;
AndroidSetGetValFunc(std::function<std::string (std::string)> func)45 void DolphinAnalytics::AndroidSetGetValFunc(std::function<std::string(std::string)> func)
46 {
47   s_get_val_func = std::move(func);
48 }
49 #endif
50 
DolphinAnalytics()51 DolphinAnalytics::DolphinAnalytics()
52 {
53   ReloadConfig();
54   MakeBaseBuilder();
55 }
56 
Instance()57 DolphinAnalytics& DolphinAnalytics::Instance()
58 {
59   static DolphinAnalytics instance;
60   return instance;
61 }
62 
ReloadConfig()63 void DolphinAnalytics::ReloadConfig()
64 {
65   std::lock_guard lk{m_reporter_mutex};
66 
67   // Install the HTTP backend if analytics support is enabled.
68   std::unique_ptr<Common::AnalyticsReportingBackend> new_backend;
69   if (Config::Get(Config::MAIN_ANALYTICS_ENABLED))
70   {
71 #if defined(ANDROID)
72     new_backend = std::make_unique<Common::AndroidAnalyticsBackend>(ANALYTICS_ENDPOINT);
73 #else
74     new_backend = std::make_unique<Common::HttpAnalyticsBackend>(ANALYTICS_ENDPOINT);
75 #endif
76   }
77   m_reporter.SetBackend(std::move(new_backend));
78 
79   // Load the unique ID or generate it if needed.
80   m_unique_id = Config::Get(Config::MAIN_ANALYTICS_ID);
81   if (m_unique_id.empty())
82   {
83     GenerateNewIdentity();
84   }
85 }
86 
GenerateNewIdentity()87 void DolphinAnalytics::GenerateNewIdentity()
88 {
89   const u64 id_high = Common::Random::GenerateValue<u64>();
90   const u64 id_low = Common::Random::GenerateValue<u64>();
91   m_unique_id = fmt::format("{:016x}{:016x}", id_high, id_low);
92 
93   // Save the new id in the configuration.
94   Config::SetBase(Config::MAIN_ANALYTICS_ID, m_unique_id);
95   Config::Save();
96 }
97 
MakeUniqueId(std::string_view data) const98 std::string DolphinAnalytics::MakeUniqueId(std::string_view data) const
99 {
100   std::array<u8, 20> digest;
101   const auto input = std::string{m_unique_id}.append(data);
102   mbedtls_sha1_ret(reinterpret_cast<const u8*>(input.c_str()), input.size(), digest.data());
103 
104   // Convert to hex string and truncate to 64 bits.
105   std::string out;
106   for (int i = 0; i < 8; ++i)
107   {
108     out += fmt::format("{:02x}", digest[i]);
109   }
110   return out;
111 }
112 
ReportDolphinStart(std::string_view ui_type)113 void DolphinAnalytics::ReportDolphinStart(std::string_view ui_type)
114 {
115   Common::AnalyticsReportBuilder builder(m_base_builder);
116   builder.AddData("type", "dolphin-start");
117   builder.AddData("ui-type", ui_type);
118   builder.AddData("id", MakeUniqueId("dolphin-start"));
119   Send(builder);
120 }
121 
ReportGameStart()122 void DolphinAnalytics::ReportGameStart()
123 {
124   MakePerGameBuilder();
125 
126   Common::AnalyticsReportBuilder builder(m_per_game_builder);
127   builder.AddData("type", "game-start");
128   Send(builder);
129 
130   // Reset per-game state.
131   m_reported_quirks.fill(false);
132   InitializePerformanceSampling();
133 }
134 
135 // Keep in sync with enum class GameQuirk definition.
136 constexpr std::array<const char*, 12> GAME_QUIRKS_NAMES{"icache-matters",
137                                                         "directly-reads-wiimote-input",
138                                                         "uses-DVDLowStopLaser",
139                                                         "uses-DVDLowOffset",
140                                                         "uses-DVDLowReadDiskBca",
141                                                         "uses-DVDLowRequestDiscStatus",
142                                                         "uses-DVDLowRequestRetryNumber",
143                                                         "uses-DVDLowSerMeasControl",
144                                                         "uses-different-partition-command",
145                                                         "uses-di-interrupt-command",
146                                                         "mismatched-gpu-texgens-between-xf-and-bp",
147                                                         "mismatched-gpu-colors-between-xf-and-bp"};
148 static_assert(GAME_QUIRKS_NAMES.size() == static_cast<u32>(GameQuirk::COUNT),
149               "Game quirks names and enum definition are out of sync.");
150 
ReportGameQuirk(GameQuirk quirk)151 void DolphinAnalytics::ReportGameQuirk(GameQuirk quirk)
152 {
153   u32 quirk_idx = static_cast<u32>(quirk);
154 
155   // Only report once per run.
156   if (m_reported_quirks[quirk_idx])
157     return;
158   m_reported_quirks[quirk_idx] = true;
159 
160   Common::AnalyticsReportBuilder builder(m_per_game_builder);
161   builder.AddData("type", "quirk");
162   builder.AddData("quirk", GAME_QUIRKS_NAMES[quirk_idx]);
163   Send(builder);
164 }
165 
ReportPerformanceInfo(PerformanceSample && sample)166 void DolphinAnalytics::ReportPerformanceInfo(PerformanceSample&& sample)
167 {
168   if (ShouldStartPerformanceSampling())
169   {
170     m_sampling_performance_info = true;
171   }
172 
173   if (m_sampling_performance_info)
174   {
175     m_performance_samples.emplace_back(std::move(sample));
176   }
177 
178   if (m_performance_samples.size() >= NUM_PERFORMANCE_SAMPLES_PER_REPORT)
179   {
180     std::vector<u32> speed_times_1000(m_performance_samples.size());
181     std::vector<u32> num_prims(m_performance_samples.size());
182     std::vector<u32> num_draw_calls(m_performance_samples.size());
183     for (size_t i = 0; i < m_performance_samples.size(); ++i)
184     {
185       speed_times_1000[i] = static_cast<u32>(m_performance_samples[i].speed_ratio * 1000);
186       num_prims[i] = m_performance_samples[i].num_prims;
187       num_draw_calls[i] = m_performance_samples[i].num_draw_calls;
188     }
189 
190     // The per game builder should already exist -- there is no way we can be reporting performance
191     // info without a game start event having been generated.
192     Common::AnalyticsReportBuilder builder(m_per_game_builder);
193     builder.AddData("type", "performance");
194     builder.AddData("speed", speed_times_1000);
195     builder.AddData("prims", num_prims);
196     builder.AddData("draw-calls", num_draw_calls);
197 
198     Send(builder);
199 
200     // Clear up and stop sampling until next time ShouldStartPerformanceSampling() says so.
201     m_performance_samples.clear();
202     m_sampling_performance_info = false;
203   }
204 }
205 
InitializePerformanceSampling()206 void DolphinAnalytics::InitializePerformanceSampling()
207 {
208   m_performance_samples.clear();
209   m_sampling_performance_info = false;
210 
211   u64 wait_us =
212       PERFORMANCE_SAMPLING_INITIAL_WAIT_TIME_SECS * 1000000 +
213       Common::Random::GenerateValue<u64>() % (PERFORMANCE_SAMPLING_WAIT_TIME_JITTER_SECS * 1000000);
214   m_sampling_next_start_us = Common::Timer::GetTimeUs() + wait_us;
215 }
216 
ShouldStartPerformanceSampling()217 bool DolphinAnalytics::ShouldStartPerformanceSampling()
218 {
219   if (Common::Timer::GetTimeUs() < m_sampling_next_start_us)
220     return false;
221 
222   u64 wait_us =
223       PERFORMANCE_SAMPLING_INTERVAL_SECS * 1000000 +
224       Common::Random::GenerateValue<u64>() % (PERFORMANCE_SAMPLING_WAIT_TIME_JITTER_SECS * 1000000);
225   m_sampling_next_start_us = Common::Timer::GetTimeUs() + wait_us;
226   return true;
227 }
228 
MakeBaseBuilder()229 void DolphinAnalytics::MakeBaseBuilder()
230 {
231   Common::AnalyticsReportBuilder builder;
232 
233   // Version information.
234   builder.AddData("version-desc", Common::scm_desc_str);
235   builder.AddData("version-hash", Common::scm_rev_git_str);
236   builder.AddData("version-branch", Common::scm_branch_str);
237   builder.AddData("version-dist", Common::scm_distributor_str);
238 
239   // Auto-Update information.
240   builder.AddData("update-track", SConfig::GetInstance().m_auto_update_track);
241 
242   // CPU information.
243   builder.AddData("cpu-summary", cpu_info.Summarize());
244 
245 // OS information.
246 #if defined(_WIN32)
247   builder.AddData("os-type", "windows");
248 
249   // Windows 8 removes support for GetVersionEx and such. Stupid.
250   DWORD(WINAPI * RtlGetVersion)(LPOSVERSIONINFOEXW);
251   *(FARPROC*)&RtlGetVersion = GetProcAddress(GetModuleHandle(TEXT("ntdll")), "RtlGetVersion");
252 
253   OSVERSIONINFOEXW winver;
254   winver.dwOSVersionInfoSize = sizeof(winver);
255   if (RtlGetVersion != nullptr)
256   {
257     RtlGetVersion(&winver);
258     builder.AddData("win-ver-major", static_cast<u32>(winver.dwMajorVersion));
259     builder.AddData("win-ver-minor", static_cast<u32>(winver.dwMinorVersion));
260     builder.AddData("win-ver-build", static_cast<u32>(winver.dwBuildNumber));
261     builder.AddData("win-ver-spmajor", static_cast<u32>(winver.wServicePackMajor));
262     builder.AddData("win-ver-spminor", static_cast<u32>(winver.wServicePackMinor));
263   }
264 #elif defined(ANDROID)
265   builder.AddData("os-type", "android");
266   builder.AddData("android-manufacturer", s_get_val_func("DEVICE_MANUFACTURER"));
267   builder.AddData("android-model", s_get_val_func("DEVICE_MODEL"));
268   builder.AddData("android-version", s_get_val_func("DEVICE_OS"));
269 #elif defined(__APPLE__)
270   builder.AddData("os-type", "osx");
271 
272   // id processInfo = [NSProcessInfo processInfo]
273   id processInfo = reinterpret_cast<id (*)(Class, SEL)>(objc_msgSend)(
274       objc_getClass("NSProcessInfo"), sel_getUid("processInfo"));
275   if (processInfo)
276   {
277     struct OSVersion  // NSOperatingSystemVersion
278     {
279       s64 major_version;  // NSInteger majorVersion
280       s64 minor_version;  // NSInteger minorVersion
281       s64 patch_version;  // NSInteger patchVersion
282     };
283 
284     // NSOperatingSystemVersion version = [processInfo operatingSystemVersion]
285     OSVersion version = reinterpret_cast<OSVersion (*)(id, SEL)>(objc_msgSend_stret)(
286         processInfo, sel_getUid("operatingSystemVersion"));
287 
288     builder.AddData("osx-ver-major", version.major_version);
289     builder.AddData("osx-ver-minor", version.minor_version);
290     builder.AddData("osx-ver-bugfix", version.patch_version);
291   }
292 #elif defined(__linux__)
293   builder.AddData("os-type", "linux");
294 #elif defined(__FreeBSD__)
295   builder.AddData("os-type", "freebsd");
296 #else
297   builder.AddData("os-type", "unknown");
298 #endif
299 
300   m_base_builder = builder;
301 }
302 
GetShaderCompilationMode(const VideoConfig & video_config)303 static const char* GetShaderCompilationMode(const VideoConfig& video_config)
304 {
305   switch (video_config.iShaderCompilationMode)
306   {
307   case ShaderCompilationMode::AsynchronousUberShaders:
308     return "async-ubershaders";
309   case ShaderCompilationMode::AsynchronousSkipRendering:
310     return "async-skip-rendering";
311   case ShaderCompilationMode::SynchronousUberShaders:
312     return "sync-ubershaders";
313   case ShaderCompilationMode::Synchronous:
314   default:
315     return "sync";
316   }
317 }
318 
MakePerGameBuilder()319 void DolphinAnalytics::MakePerGameBuilder()
320 {
321   Common::AnalyticsReportBuilder builder(m_base_builder);
322 
323   // Gameid.
324   builder.AddData("gameid", SConfig::GetInstance().GetGameID());
325 
326   // Unique id bound to the gameid.
327   builder.AddData("id", MakeUniqueId(SConfig::GetInstance().GetGameID()));
328 
329   // Configuration.
330   builder.AddData("cfg-dsp-hle", SConfig::GetInstance().bDSPHLE);
331   builder.AddData("cfg-dsp-jit", SConfig::GetInstance().m_DSPEnableJIT);
332   builder.AddData("cfg-dsp-thread", SConfig::GetInstance().bDSPThread);
333   builder.AddData("cfg-cpu-thread", SConfig::GetInstance().bCPUThread);
334   builder.AddData("cfg-fastmem", SConfig::GetInstance().bFastmem);
335   builder.AddData("cfg-syncgpu", SConfig::GetInstance().bSyncGPU);
336   builder.AddData("cfg-audio-backend", SConfig::GetInstance().sBackend);
337   builder.AddData("cfg-oc-enable", SConfig::GetInstance().m_OCEnable);
338   builder.AddData("cfg-oc-factor", SConfig::GetInstance().m_OCFactor);
339   builder.AddData("cfg-render-to-main", Config::Get(Config::MAIN_RENDER_TO_MAIN));
340   if (g_video_backend)
341   {
342     builder.AddData("cfg-video-backend", g_video_backend->GetName());
343   }
344 
345   // Video configuration.
346   builder.AddData("cfg-gfx-multisamples", g_Config.iMultisamples);
347   builder.AddData("cfg-gfx-ssaa", g_Config.bSSAA);
348   builder.AddData("cfg-gfx-anisotropy", g_Config.iMaxAnisotropy);
349   builder.AddData("cfg-gfx-vsync", g_Config.bVSync);
350   builder.AddData("cfg-gfx-aspect-ratio", static_cast<int>(g_Config.aspect_mode));
351   builder.AddData("cfg-gfx-efb-access", g_Config.bEFBAccessEnable);
352   builder.AddData("cfg-gfx-efb-copy-format-changes", g_Config.bEFBEmulateFormatChanges);
353   builder.AddData("cfg-gfx-efb-copy-ram", !g_Config.bSkipEFBCopyToRam);
354   builder.AddData("cfg-gfx-xfb-copy-ram", !g_Config.bSkipXFBCopyToRam);
355   builder.AddData("cfg-gfx-defer-efb-copies", g_Config.bDeferEFBCopies);
356   builder.AddData("cfg-gfx-immediate-xfb", !g_Config.bImmediateXFB);
357   builder.AddData("cfg-gfx-efb-copy-scaled", g_Config.bCopyEFBScaled);
358   builder.AddData("cfg-gfx-internal-resolution", g_Config.iEFBScale);
359   builder.AddData("cfg-gfx-tc-samples", g_Config.iSafeTextureCache_ColorSamples);
360   builder.AddData("cfg-gfx-stereo-mode", static_cast<int>(g_Config.stereo_mode));
361   builder.AddData("cfg-gfx-per-pixel-lighting", g_Config.bEnablePixelLighting);
362   builder.AddData("cfg-gfx-shader-compilation-mode", GetShaderCompilationMode(g_Config));
363   builder.AddData("cfg-gfx-wait-for-shaders", g_Config.bWaitForShadersBeforeStarting);
364   builder.AddData("cfg-gfx-fast-depth", g_Config.bFastDepthCalc);
365   builder.AddData("cfg-gfx-vertex-rounding", g_Config.UseVertexRounding());
366 
367   // GPU features.
368   if (g_Config.iAdapter < static_cast<int>(g_Config.backend_info.Adapters.size()))
369   {
370     builder.AddData("gpu-adapter", g_Config.backend_info.Adapters[g_Config.iAdapter]);
371   }
372   else if (!g_Config.backend_info.AdapterName.empty())
373   {
374     builder.AddData("gpu-adapter", g_Config.backend_info.AdapterName);
375   }
376   builder.AddData("gpu-has-exclusive-fullscreen",
377                   g_Config.backend_info.bSupportsExclusiveFullscreen);
378   builder.AddData("gpu-has-dual-source-blend", g_Config.backend_info.bSupportsDualSourceBlend);
379   builder.AddData("gpu-has-primitive-restart", g_Config.backend_info.bSupportsPrimitiveRestart);
380   builder.AddData("gpu-has-oversized-viewports", g_Config.backend_info.bSupportsOversizedViewports);
381   builder.AddData("gpu-has-geometry-shaders", g_Config.backend_info.bSupportsGeometryShaders);
382   builder.AddData("gpu-has-3d-vision", g_Config.backend_info.bSupports3DVision);
383   builder.AddData("gpu-has-early-z", g_Config.backend_info.bSupportsEarlyZ);
384   builder.AddData("gpu-has-binding-layout", g_Config.backend_info.bSupportsBindingLayout);
385   builder.AddData("gpu-has-bbox", g_Config.backend_info.bSupportsBBox);
386   builder.AddData("gpu-has-fragment-stores-and-atomics",
387                   g_Config.backend_info.bSupportsFragmentStoresAndAtomics);
388   builder.AddData("gpu-has-gs-instancing", g_Config.backend_info.bSupportsGSInstancing);
389   builder.AddData("gpu-has-post-processing", g_Config.backend_info.bSupportsPostProcessing);
390   builder.AddData("gpu-has-palette-conversion", g_Config.backend_info.bSupportsPaletteConversion);
391   builder.AddData("gpu-has-clip-control", g_Config.backend_info.bSupportsClipControl);
392   builder.AddData("gpu-has-ssaa", g_Config.backend_info.bSupportsSSAA);
393 
394   // NetPlay / recording.
395   builder.AddData("netplay", NetPlay::IsNetPlayRunning());
396   builder.AddData("movie", Movie::IsMovieActive());
397 
398   // Controller information
399   // We grab enough to tell what percentage of our users are playing with keyboard/mouse, some kind
400   // of gamepad
401   // or the official gamecube adapter.
402   builder.AddData("gcadapter-detected", GCAdapter::IsDetected(nullptr));
403   builder.AddData("has-controller", Pad::GetConfig()->IsControllerControlledByGamepadDevice(0) ||
404                                         GCAdapter::IsDetected(nullptr));
405 
406   m_per_game_builder = builder;
407 }
408