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