1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3 
4 #include "PerfInfo.h"
5 #include "Frame.h"
6 #include "Game.h"
7 #include "LuaPiGui.h"
8 #include "Pi.h"
9 #include "Player.h"
10 #include "Space.h"
11 #include "graphics/Renderer.h"
12 #include "graphics/Stats.h"
13 #include "graphics/Texture.h"
14 #include "lua/Lua.h"
15 #include "lua/LuaManager.h"
16 #include "scenegraph/Model.h"
17 #include "text/TextureFont.h"
18 
19 #include <imgui/imgui.h>
20 #include <algorithm>
21 #include <cstddef>
22 #include <fstream>
23 #include <functional>
24 #include <sstream>
25 
26 #ifdef _WIN32
27 #include <windows.h>
28 // order of header includes matters, thanks Windows.h!
29 #include <psapi.h>
30 #endif
31 
32 // using namespace PiGui;
33 using PerfInfo = PiGui::PerfInfo;
34 
35 struct PerfInfo::ImGuiState {
36 	bool perfWindowOpen = true;
37 	bool updatePause = false;
38 	bool metricsWindowOpen = false;
39 	uint32_t playerModelDebugFlags = 0;
40 
41 	bool textureCacheViewerOpen = false;
42 
43 	std::map<std::string, std::vector<std::string>> indirectionMap;
44 
45 	bool hasSelectedTexture = false;
46 	std::pair<std::string, std::string> selectedTexture;
47 };
48 
PerfInfo()49 PerfInfo::PerfInfo() :
50 	m_state(new ImGuiState({}))
51 {
52 	m_fpsGraph.fill(0.0);
53 	m_physFpsGraph.fill(0.0);
54 }
55 
~PerfInfo()56 PerfInfo::~PerfInfo()
57 {
58 	delete m_state;
59 }
60 
61 // ============================================================================
62 //
63 // Hardware Performance Information
64 //
65 // ============================================================================
66 
67 #define ignoreLine(f) f.ignore(std::numeric_limits<std::streamsize>::max(), '\n')
GetMemoryInfo()68 static PerfInfo::MemoryInfo GetMemoryInfo()
69 {
70 	PerfInfo::MemoryInfo ret{};
71 #if __linux__
72 	std::ifstream statusFile("/proc/self/status");
73 	while (statusFile.good() && !statusFile.eof()) {
74 		if (statusFile.peek() != 'V') {
75 			ignoreLine(statusFile);
76 			continue;
77 		}
78 
79 		char statName[16] = { 0 };
80 		statusFile.getline(statName, 16, ':');
81 
82 		if (strcmp(statName, "VmHWM") == 0) {
83 			statusFile >> ret.peakMemSize;
84 		} else if (strcmp(statName, "VmRSS") == 0) {
85 			statusFile >> ret.currentMemSize;
86 		}
87 
88 		ignoreLine(statusFile);
89 	}
90 	statusFile.close();
91 
92 #elif _WIN32
93 	// Get win32 memory count
94 	PROCESS_MEMORY_COUNTERS pmc;
95 	if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
96 		// convert from bytes to kilobytes
97 		ret.peakMemSize = pmc.PeakWorkingSetSize / 1024;
98 		ret.currentMemSize = pmc.WorkingSetSize / 1024;
99 	}
100 #elif __APPLE__
101 	// TODO: get OSX memory count
102 #endif
103 
104 	return ret;
105 }
106 #undef ignoreLine
107 
Update(float deltaTime,float physTime)108 void PerfInfo::Update(float deltaTime, float physTime)
109 {
110 	// Don't accumulate new frames when performance data is paused.
111 	if (m_state->updatePause)
112 		return;
113 
114 	// Drop the oldest frame, make room for the new frame.
115 	std::move(m_fpsGraph.begin() + 1, m_fpsGraph.end(), m_fpsGraph.begin());
116 	std::move(m_physFpsGraph.begin() + 1, m_physFpsGraph.end(), m_physFpsGraph.begin());
117 	m_fpsGraph[NUM_FRAMES - 1] = deltaTime;
118 	m_physFpsGraph[NUM_FRAMES - 1] = physTime;
119 
120 	float fpsAccum = 0;
121 	frameTimeMax = 0.f;
122 	frameTimeMin = 0.f;
123 	std::for_each(m_fpsGraph.begin(), m_fpsGraph.end(), [&](float i) {
124 		fpsAccum += i;
125 		frameTimeMax = std::max(frameTimeMax, i);
126 		frameTimeMin = std::min(frameTimeMin, i);
127 	});
128 	frameTimeAverage = fpsAccum / double(NUM_FRAMES);
129 
130 	float physFpsAccum = 0;
131 	physFrameTimeMax = 0.f;
132 	physFrameTimeMin = 0.f;
133 	std::for_each(m_physFpsGraph.begin(), m_physFpsGraph.end(), [&](float i) {
134 		physFpsAccum += i;
135 		physFrameTimeMax = std::max(physFrameTimeMax, i);
136 		physFrameTimeMin = std::min(physFrameTimeMin, i);
137 	});
138 	physFrameTimeAverage = physFpsAccum / double(NUM_FRAMES);
139 
140 	lastUpdateTime += deltaTime;
141 	if (lastUpdateTime > 1000.0) {
142 		lastUpdateTime = fmod(lastUpdateTime, 1000.0);
143 
144 		lua_mem = ::Lua::manager->GetMemoryUsage();
145 		process_mem = GetMemoryInfo();
146 	}
147 }
148 
149 // TODO: evaluate whether this method of tracking FPS is necessary.
UpdateFrameInfo(int fS,int pfS)150 void PerfInfo::UpdateFrameInfo(int fS, int pfS)
151 {
152 	framesThisSecond = fS;
153 	physFramesThisSecond = pfS;
154 }
155 
SetUpdatePause(bool pause)156 void PerfInfo::SetUpdatePause(bool pause)
157 {
158 	m_state->updatePause = pause;
159 }
160 
161 // ============================================================================
162 //
163 // Main Performance Window
164 //
165 // ============================================================================
166 
GetImTextureID(const Graphics::Texture * tex)167 ImTextureID GetImTextureID(const Graphics::Texture *tex)
168 {
169 	return reinterpret_cast<ImTextureID>(tex->GetTextureID() | 0UL);
170 }
171 
172 namespace ImGui {
Value(const char * prefix,const std::string & str)173 	IMGUI_API void Value(const char *prefix, const std::string &str)
174 	{
175 		ImGui::TextWrapped("%s: %s", prefix, str.c_str());
176 	}
177 } // namespace ImGui
178 
179 static constexpr double scale_MB = 1024.0 * 1024.0;
180 
Draw()181 void PerfInfo::Draw()
182 {
183 	if (m_state->perfWindowOpen)
184 		DrawPerfWindow();
185 
186 	if (m_state->textureCacheViewerOpen)
187 		DrawTextureCache();
188 
189 	if (m_state->hasSelectedTexture &&
190 		Pi::renderer->GetCachedTexture(m_state->selectedTexture.first, m_state->selectedTexture.second))
191 		DrawTextureInspector();
192 
193 	if (m_state->metricsWindowOpen)
194 		ImGui::ShowMetricsWindow(&m_state->metricsWindowOpen);
195 }
196 
DrawPerfWindow()197 void PerfInfo::DrawPerfWindow()
198 {
199 	if (ImGui::Begin("Performance", nullptr, ImGuiWindowFlags_NoNav)) {
200 		ImGui::Text("%.1f fps (%.1f ms) %.1f physics ups (%.1f ms/u)", framesThisSecond, frameTimeAverage, physFramesThisSecond, physFrameTimeAverage);
201 		ImGui::PlotLines("Frame Time (ms)", m_fpsGraph.data(), m_fpsGraph.size(), 0, nullptr, 0.0, 33.0, { 0, 60 });
202 		ImGui::PlotLines("Update Time (ms)", m_physFpsGraph.data(), m_physFpsGraph.size(), 0, nullptr, 0.0, 10.0, { 0, 25 });
203 		if (ImGui::Button(m_state->updatePause ? "Unpause" : "Pause")) {
204 			SetUpdatePause(!m_state->updatePause);
205 		}
206 
207 		if (process_mem.currentMemSize)
208 			ImGui::Text("%.1f MB process memory usage (%.1f MB peak)", (process_mem.currentMemSize * 1e-3), (process_mem.peakMemSize * 1e-3));
209 		ImGui::Text("%.3f MB Lua memory usage", double(lua_mem) / scale_MB);
210 		ImGui::Spacing();
211 
212 		if (ImGui::BeginTabBar("PerfInfoTabs")) {
213 			if (ImGui::BeginTabItem("Renderer")) {
214 				DrawRendererStats();
215 				DrawImGuiStats();
216 				ImGui::EndTabItem();
217 			}
218 
219 			if (false && ImGui::BeginTabItem("Input")) {
220 				DrawInputDebug();
221 				ImGui::EndTabItem();
222 			}
223 
224 			if (Pi::game) {
225 				if (Pi::player->GetFlightState() != Ship::HYPERSPACE && ImGui::BeginTabItem("WorldView")) {
226 					DrawWorldViewStats();
227 					ImGui::EndTabItem();
228 				}
229 			}
230 
231 			PiGui::RunHandler(Pi::GetFrameTime(), "debug-tabs");
232 
233 			ImGui::EndTabBar();
234 		}
235 	}
236 
237 	ImGui::End();
238 
239 	PiGui::RunHandler(Pi::GetFrameTime(), "debug");
240 }
241 
DrawRendererStats()242 void PerfInfo::DrawRendererStats()
243 {
244 	const Graphics::Stats::TFrameData &stats = Pi::renderer->GetStats().FrameStatsPrevious();
245 	const Uint32 numDrawCalls = stats.m_stats[Graphics::Stats::STAT_DRAWCALL];
246 	const Uint32 numBuffersCreated = stats.m_stats[Graphics::Stats::STAT_CREATE_BUFFER];
247 	const Uint32 numBuffersInUse = stats.m_stats[Graphics::Stats::STAT_BUFFER_INUSE];
248 	const Uint32 numDrawTris = stats.m_stats[Graphics::Stats::STAT_DRAWTRIS];
249 	const Uint32 numDrawPointSprites = stats.m_stats[Graphics::Stats::STAT_DRAWPOINTSPRITES];
250 	const Uint32 numDrawBuildings = stats.m_stats[Graphics::Stats::STAT_BUILDINGS];
251 	const Uint32 numDrawCities = stats.m_stats[Graphics::Stats::STAT_CITIES];
252 	const Uint32 numDrawGroundStations = stats.m_stats[Graphics::Stats::STAT_GROUNDSTATIONS];
253 	const Uint32 numDrawSpaceStations = stats.m_stats[Graphics::Stats::STAT_SPACESTATIONS];
254 	const Uint32 numDrawAtmospheres = stats.m_stats[Graphics::Stats::STAT_ATMOSPHERES];
255 	const Uint32 numDrawPlanets = stats.m_stats[Graphics::Stats::STAT_PLANETS];
256 	const Uint32 numDrawGasGiants = stats.m_stats[Graphics::Stats::STAT_GASGIANTS];
257 	const Uint32 numDrawStars = stats.m_stats[Graphics::Stats::STAT_STARS];
258 	const Uint32 numDrawShips = stats.m_stats[Graphics::Stats::STAT_SHIPS];
259 	const Uint32 numDrawBillBoards = stats.m_stats[Graphics::Stats::STAT_BILLBOARD];
260 
261 	const Uint32 numTex2ds = stats.m_stats[Graphics::Stats::STAT_NUM_TEXTURE2D];
262 	const Uint32 tex2dMemUsage = stats.m_stats[Graphics::Stats::STAT_MEM_TEXTURE2D];
263 	const Uint32 numTexCubemaps = stats.m_stats[Graphics::Stats::STAT_NUM_TEXTURECUBE];
264 	const Uint32 texCubeMemUsage = stats.m_stats[Graphics::Stats::STAT_MEM_TEXTURECUBE];
265 	const Uint32 numTexArray2ds = stats.m_stats[Graphics::Stats::STAT_NUM_TEXTUREARRAY2D];
266 	const Uint32 texArray2dMemUsage = stats.m_stats[Graphics::Stats::STAT_MEM_TEXTUREARRAY2D];
267 	const Uint32 numCachedTextures = numTex2ds + numTexCubemaps + numTexArray2ds;
268 	const Uint32 cachedTextureMemUsage = tex2dMemUsage + texCubeMemUsage + texArray2dMemUsage;
269 
270 	ImGui::Text("Renderer:");
271 	ImGui::Text("%d tris (%.3f M tris/sec), %d GeoPatches, %d glyphs",
272 		Pi::statSceneTris, Pi::statSceneTris * framesThisSecond * 1e-6, Pi::statNumPatches, Text::TextureFont::GetGlyphCount());
273 	ImGui::Text("%u draw calls (%u tris, %u point sprites, %u billboards)",
274 		numDrawCalls, numDrawTris, numDrawPointSprites, numDrawBillBoards);
275 	ImGui::Text("%u Buildings, %u Cities, %u Gd.Stations, %u Sp.Stations",
276 		numDrawBuildings, numDrawCities, numDrawGroundStations, numDrawSpaceStations);
277 	ImGui::Text("%u Atmospheres, %u Planets, %u Gas Giants, %u Stars, %u Ships",
278 		numDrawAtmospheres, numDrawPlanets, numDrawGasGiants, numDrawStars, numDrawShips);
279 	ImGui::Text("%u Buffers Created (%u in use)", numBuffersCreated, numBuffersInUse);
280 	ImGui::Spacing();
281 
282 	ImGui::Text("%u cached textures, using %.3f MB VRAM", numCachedTextures, double(cachedTextureMemUsage) / scale_MB);
283 
284 	if (ImGui::Button("Open Texture Cache Visualizer"))
285 		m_state->textureCacheViewerOpen = true;
286 
287 	if (ImGui::Button("Reload Shaders"))
288 		Pi::renderer->ReloadShaders();
289 
290 	ImGui::Text("%u Texture2D in cache (%.3f MB)", numTex2ds, double(tex2dMemUsage) / scale_MB);
291 	ImGui::Text("%u Cubemaps in cache (%.3f MB)", numTexCubemaps, double(texCubeMemUsage) / scale_MB);
292 	ImGui::Text("%u TextureArray2D in cache (%.3f MB)", numTexArray2ds, double(texArray2dMemUsage) / scale_MB);
293 }
294 
DrawWorldViewStats()295 void PerfInfo::DrawWorldViewStats()
296 {
297 	vector3d pos = Pi::player->GetPosition();
298 	vector3d abs_pos = Pi::player->GetPositionRelTo(Pi::game->GetSpace()->GetRootFrame());
299 
300 	const FrameId playerFrame = Pi::player->GetFrame();
301 
302 	ImGui::TextUnformatted(fmt::format("Player Position: {:.5}, {:.5}, {:.5}", pos.x, pos.y, pos.z).c_str());
303 	ImGui::TextUnformatted(fmt::format("Absolute Position: {:.5}, {:.5}, {:.5}", abs_pos.x, abs_pos.y, abs_pos.z).c_str());
304 
305 	const Frame *frame = Frame::GetFrame(playerFrame);
306 	const SystemPath &path(frame->GetSystemBody()->GetPath());
307 
308 	std::string tempStr;
309 	tempStr = fmt::format("Relative to frame: {} [{}, {}, {}, {}, {}]",
310 		frame->GetLabel(), path.sectorX, path.sectorY, path.sectorZ, path.systemIndex, path.bodyIndex);
311 
312 	ImGui::TextUnformatted(tempStr.c_str());
313 
314 	tempStr = fmt::format("Distance from frame: {:.2f} km, rotating: {}, has rotation: {}",
315 		pos.Length() / 1000.0, frame->IsRotFrame(), frame->HasRotFrame());
316 
317 	ImGui::TextUnformatted(tempStr.c_str());
318 
319 	ImGui::Spacing();
320 
321 	//Calculate lat/lon for ship position
322 	const vector3d dir = pos.NormalizedSafe();
323 	const float lat = RAD2DEG(asin(dir.y));
324 	const float lon = RAD2DEG(atan2(dir.x, dir.z));
325 
326 	ImGui::TextUnformatted(fmt::format("Lat / Lon: {:.8} / {:.8}", lat, lon).c_str());
327 
328 	char aibuf[256];
329 	Pi::player->AIGetStatusText(aibuf);
330 
331 	ImGui::TextUnformatted(aibuf);
332 
333 	ImGui::Spacing();
334 	ImGui::TextUnformatted("Player Model ShowFlags:");
335 
336 	using Flags = SceneGraph::Model::DebugFlags;
337 
338 	bool showColl = m_state->playerModelDebugFlags & Flags::DEBUG_COLLMESH;
339 	bool showBBox = m_state->playerModelDebugFlags & Flags::DEBUG_BBOX;
340 	bool showTags = m_state->playerModelDebugFlags & Flags::DEBUG_TAGS;
341 
342 	bool changed = ImGui::Checkbox("Show Collision Mesh", &showColl);
343 	changed |= ImGui::Checkbox("Show Bounding Box", &showBBox);
344 	changed |= ImGui::Checkbox("Show Tag Locations", &showTags);
345 
346 	/* clang-format off */
347 	if (changed) {
348 		m_state->playerModelDebugFlags = (showColl ? Flags::DEBUG_COLLMESH : 0)
349 			| (showBBox ? Flags::DEBUG_BBOX : 0)
350 			| (showTags ? Flags::DEBUG_TAGS : 0);
351 		Pi::player->GetModel()->SetDebugFlags(m_state->playerModelDebugFlags);
352 	}
353 	/* clang-format on */
354 
355 	if (Pi::player->GetNavTarget() && Pi::player->GetNavTarget()->GetSystemBody()) {
356 		const auto *sbody = Pi::player->GetNavTarget()->GetSystemBody();
357 		ImGui::TextUnformatted(fmt::format("Name: {}, Population: {}", sbody->GetName(), sbody->GetPopulation() * 1e9).c_str());
358 	}
359 }
360 
DrawInputDebug()361 void PerfInfo::DrawInputDebug()
362 {
363 	std::ostringstream output;
364 	auto frameList = Pi::input->GetInputFrames();
365 	uint32_t index = 0;
366 	for (const auto *frame : frameList) {
367 		ImGui::Text("InputFrame %d: modal=%d", index, frame->modal);
368 		ImGui::Indent();
369 		uint32_t actionNum = 0;
370 		for (const auto *action : frame->actions) {
371 			ImGui::Text("Action %d: active=%d (%d, %d)", actionNum, action->m_active, action->binding.m_active, action->binding2.m_active);
372 			if (action->binding.Enabled()) {
373 				output << action->binding;
374 				ImGui::TextUnformatted(output.str().c_str());
375 				output.str("");
376 			}
377 			if (action->binding2.Enabled()) {
378 				output << action->binding2;
379 				ImGui::TextUnformatted(output.str().c_str());
380 				output.str("");
381 			}
382 
383 			ImGui::Separator();
384 			actionNum++;
385 		}
386 
387 		ImGui::Spacing();
388 
389 		actionNum = 0;
390 		for (const auto *axis : frame->axes) {
391 			ImGui::Text("Axis %d: value=%.2f (%d, %d)", actionNum, axis->m_value, axis->positive.m_active, axis->negative.m_active);
392 			if (axis->positive.Enabled()) {
393 				output << axis->positive;
394 				ImGui::TextUnformatted(output.str().c_str());
395 				output.str("");
396 			}
397 
398 			if (axis->negative.Enabled()) {
399 				output << axis->negative;
400 				ImGui::TextUnformatted(output.str().c_str());
401 				output.str("");
402 			}
403 
404 			ImGui::Separator();
405 			actionNum++;
406 		}
407 		ImGui::Unindent();
408 
409 		ImGui::Spacing();
410 		index++;
411 	}
412 }
413 
DrawImGuiStats()414 void PerfInfo::DrawImGuiStats()
415 {
416 	ImGui::NewLine();
417 	auto &io = ImGui::GetIO();
418 	ImGui::Text("ImGui stats:");
419 	ImGui::Text("%d verts, %d tris", io.MetricsRenderVertices, io.MetricsRenderIndices / 3);
420 	ImGui::Text("%d active windows (%d visible)", io.MetricsActiveWindows, io.MetricsRenderWindows);
421 	ImGui::Text("%d current allocations", io.MetricsActiveAllocations);
422 
423 	if (ImGui::Button("Toggle Metrics Window")) {
424 		m_state->metricsWindowOpen = !m_state->metricsWindowOpen;
425 	}
426 }
427 
DrawStatList(const Perf::Stats::FrameInfo & fi)428 void PerfInfo::DrawStatList(const Perf::Stats::FrameInfo &fi)
429 {
430 	ImGui::BeginChild("FrameInfo");
431 	ImGui::Columns(2);
432 	for (auto &t : fi) {
433 		ImGui::Text("%s:", t.first.c_str());
434 		ImGui::NextColumn();
435 		ImGui::Text("%'d", t.second);
436 		ImGui::NextColumn();
437 	}
438 	ImGui::Columns();
439 	ImGui::EndChild();
440 }
441 
442 // ============================================================================
443 //
444 // Texture Cache Visualizer
445 //
446 // ============================================================================
447 
DrawTexture(PerfInfo::ImGuiState * m_state,const Graphics::Texture * tex)448 bool DrawTexture(PerfInfo::ImGuiState *m_state, const Graphics::Texture *tex)
449 {
450 	ImGui::BeginGroup();
451 	auto pos0 = ImGui::GetCursorPos();
452 
453 	const vector2f texUVs = tex->GetDescriptor().texSize;
454 	ImGui::Image(GetImTextureID(tex), { 128, 128 }, { 0, 0 }, { texUVs.x, texUVs.y });
455 
456 	auto pos1 = ImGui::GetCursorPos();
457 	ImGui::SetCursorPos(pos0);
458 
459 	auto texSize = tex->GetDescriptor().dataSize;
460 	ImGui::Text("%ux%u", uint32_t(texSize.x), uint32_t(texSize.y));
461 	ImGui::Text("%.1f KB", double(tex->GetTextureMemSize()) / 1024.0);
462 
463 	ImGui::SetCursorPos(pos1);
464 	ImGui::EndGroup();
465 	return ImGui::IsItemClicked();
466 }
467 
DrawTextureCache()468 void PerfInfo::DrawTextureCache()
469 {
470 	if (ImGui::Begin("Texture Cache Viewer", &m_state->textureCacheViewerOpen, ImGuiWindowFlags_NoNav)) {
471 		for (auto &v : m_state->indirectionMap)
472 			// purge any textures which may have changed / evicted, but don't resize the vector
473 			// in the average case, this will simply re-fill the vector each frame, a fairly trivial operation.
474 			v.second.clear();
475 
476 		for (const auto &t : Pi::renderer->GetTextureCache()) {
477 			const auto &key = t.first;
478 			if (t.second->Get()->GetDescriptor().type != Graphics::TEXTURE_2D)
479 				continue;
480 
481 			m_state->indirectionMap[key.first].push_back(key.second);
482 		}
483 
484 		if (ImGui::BeginTabBar("Texture List")) {
485 			const int item_width = 128 + ImGui::GetStyle().ItemSpacing.x;
486 			for (const auto &t : m_state->indirectionMap) {
487 				if (!ImGui::BeginTabItem(t.first.c_str()))
488 					continue;
489 
490 				if (ImGui::BeginChild("##Texture List Scroll")) {
491 					const int num_columns = std::max(int(ImGui::GetWindowContentRegionWidth()) / item_width, 1);
492 					ImGui::Columns(num_columns);
493 					if (num_columns > 1) {
494 						for (int idx = 0; idx < num_columns; idx++)
495 							ImGui::SetColumnWidth(idx, item_width);
496 					}
497 
498 					for (const std::string &name : t.second) {
499 						const auto *ptr = Pi::renderer->GetCachedTexture(t.first, name);
500 						if (DrawTexture(m_state, ptr)) {
501 							m_state->selectedTexture = { t.first, name };
502 							m_state->hasSelectedTexture = true;
503 						}
504 
505 						if (ImGui::IsItemHovered())
506 							ImGui::SetTooltip("%s", name.c_str());
507 
508 						if (num_columns > 1)
509 							ImGui::NextColumn();
510 					}
511 				}
512 				ImGui::EndChild();
513 
514 				ImGui::EndTabItem();
515 			}
516 
517 			ImGui::EndTabBar();
518 		}
519 	}
520 	ImGui::End();
521 }
522 
DrawTextureInspector()523 void PerfInfo::DrawTextureInspector()
524 {
525 	const auto &selectedTex = m_state->selectedTexture;
526 	const auto *tex = Pi::renderer->GetCachedTexture(selectedTex.first, selectedTex.second);
527 	if (tex != nullptr && m_state->hasSelectedTexture) {
528 		ImGui::SetNextWindowSize({ 300, 400 }, ImGuiCond_Appearing);
529 		std::string window_name = selectedTex.second + "###Texture Inspector";
530 		if (ImGui::Begin(window_name.c_str(), &m_state->hasSelectedTexture)) {
531 			const Graphics::TextureDescriptor &descriptor = tex->GetDescriptor();
532 
533 			// If we have POT-extended textures, only display the part of the texture corresponding to the actual texture data.
534 			const vector2f texUVs = tex->GetDescriptor().texSize;
535 			ImGui::Image(GetImTextureID(tex), { 256, 256 }, { 0, 0 }, { texUVs.x, texUVs.y });
536 			ImGui::Text("Dimensions: %ux%u", uint32_t(descriptor.dataSize.x), uint32_t(descriptor.dataSize.y));
537 			ImGui::Value("Mipmap Count", tex->GetDescriptor().numberOfMipMaps);
538 			ImGui::Value("VRAM Size", tex->GetTextureMemSize());
539 			if (texUVs.x < 1.0 || texUVs.y < 1.0)
540 				ImGui::Text("Original Size: %ux%u", int(texUVs.x * descriptor.dataSize.x), int(texUVs.y * descriptor.dataSize.y));
541 
542 			ImGui::Spacing();
543 			ImGui::Value("Cache Bucket", selectedTex.first);
544 			ImGui::Value("Cache ID", selectedTex.second);
545 			ImGui::Value("TextureID", tex->GetTextureID());
546 			ImGui::Value("RefCount", tex->GetRefCount());
547 		}
548 		ImGui::End();
549 	}
550 }
551