From 06f13f3cb1d904065051635bc8c81fa96d48aef1 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Sun, 17 Aug 2025 15:48:06 +1000 Subject: [PATCH] android: Add shader building overlay with performance graph - Add new settings for shader building overlay and performance graph - Create ShaderBuildingOverlayView with animated shader building indicator - Implement JNI bridge to get shader building count from core - Add performance metrics display (FPS, frametime, emulation speed) - Include real-time frametime graph with min/avg/max statistics - Add menu options to toggle overlay and graph independently - Integrate with existing overlay system in EmulationFragment - Optimize Vulkan pipeline cache loading with pre-reservation - Improve async shader building to reduce main thread blocking Signed-off-by: Zephyron --- .../org/citron/citron_emu/NativeLibrary.kt | 5 + .../features/settings/model/BooleanSetting.kt | 4 +- .../citron_emu/fragments/EmulationFragment.kt | 80 ++++++ .../views/ShaderBuildingOverlayView.kt | 267 ++++++++++++++++++ .../app/src/main/jni/android_settings.h | 4 + src/android/app/src/main/jni/native.cpp | 8 + .../main/res/layout/fragment_emulation.xml | 9 + .../main/res/menu/menu_overlay_options.xml | 10 + .../app/src/main/res/values/strings.xml | 2 + .../renderer_opengl/gl_shader_cache.cpp | 33 ++- .../renderer_vulkan/vk_pipeline_cache.cpp | 36 ++- 11 files changed, 429 insertions(+), 29 deletions(-) create mode 100644 src/android/app/src/main/java/org/citron/citron_emu/views/ShaderBuildingOverlayView.kt diff --git a/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt index fc63dc276..0dd1f6157 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt @@ -161,6 +161,11 @@ object NativeLibrary { */ external fun getPerfStats(): DoubleArray + /** + * Returns the number of shaders currently being built + */ + external fun getShadersBuilding(): Int + /** * Returns the current CPU backend. */ diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt index a18a1a70e..f0df7c4f8 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt @@ -28,7 +28,9 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { SHOW_INPUT_OVERLAY("show_input_overlay"), TOUCHSCREEN("touchscreen"), SHOW_THERMAL_OVERLAY("show_thermal_overlay"), - SHOW_RAM_METER("show_ram_meter"); + SHOW_RAM_METER("show_ram_meter"), + SHOW_SHADER_BUILDING_OVERLAY("show_shader_building_overlay"), + SHOW_PERFORMANCE_GRAPH("show_performance_graph"); override fun getBoolean(needsGlobal: Boolean): Boolean = NativeConfig.getBoolean(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt index ae00816ef..f9479354d 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/fragments/EmulationFragment.kt @@ -70,6 +70,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private var perfStatsUpdater: (() -> Unit)? = null private var thermalStatsUpdater: (() -> Unit)? = null private var ramStatsUpdater: (() -> Unit)? = null + private var shaderStatsUpdater: (() -> Unit)? = null private var _binding: FragmentEmulationBinding? = null private val binding get() = _binding!! @@ -379,6 +380,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { updateShowFpsOverlay() updateThermalOverlay() updateRamMeterOverlay() + updateShaderBuildingOverlay() } } emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { @@ -389,6 +391,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { ViewUtils.hideView(binding.fpsIndicatorView) ViewUtils.hideView(binding.thermalIndicatorView) ViewUtils.hideView(binding.ramMeterView) + ViewUtils.hideView(binding.shaderBuildingOverlayView) } } emulationViewModel.drawerOpen.collect(viewLifecycleOwner) { @@ -414,6 +417,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (perfStatsUpdater != null) { perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) } + if (thermalStatsUpdater != null) { + thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) + } + if (ramStatsUpdater != null) { + ramStatsUpdateHandler.removeCallbacks(ramStatsUpdater!!) + } + if (shaderStatsUpdater != null) { + shaderStatsUpdateHandler.removeCallbacks(shaderStatsUpdater!!) + } emulationState.changeProgram(emulationViewModel.programChanged.value) emulationViewModel.setProgramChanged(-1) emulationViewModel.setEmulationStopped(false) @@ -479,6 +491,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } override fun onDetach() { + // Clean up all updaters + if (perfStatsUpdater != null) { + perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) + } + if (thermalStatsUpdater != null) { + thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) + } + if (ramStatsUpdater != null) { + ramStatsUpdateHandler.removeCallbacks(ramStatsUpdater!!) + } + if (shaderStatsUpdater != null) { + shaderStatsUpdateHandler.removeCallbacks(shaderStatsUpdater!!) + } + NativeLibrary.clearEmulationActivity() super.onDetach() } @@ -560,6 +586,41 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + private fun updateShaderBuildingOverlay() { + val showOverlay = BooleanSetting.SHOW_SHADER_BUILDING_OVERLAY.getBoolean() + val showGraph = BooleanSetting.SHOW_PERFORMANCE_GRAPH.getBoolean() + binding.shaderBuildingOverlayView.setVisible(showOverlay || showGraph) + + if (showOverlay || showGraph) { + shaderStatsUpdater = { + if (emulationViewModel.emulationStarted.value && + !emulationViewModel.isEmulationStopping.value + ) { + if (_binding != null) { + val perfStats = NativeLibrary.getPerfStats() + val shadersBuilding = NativeLibrary.getShadersBuilding() + + // perfStats[0] = system_fps, perfStats[1] = average_game_fps, + // perfStats[2] = frametime, perfStats[3] = emulation_speed + val fps = perfStats[1].toFloat() + val frameTime = (perfStats[2] * 1000).toFloat() // Convert to milliseconds + val speed = (perfStats[3] * 100).toFloat() // Convert to percentage + + binding.shaderBuildingOverlayView.updatePerformanceStats( + fps, frameTime, speed, shadersBuilding + ) + } + shaderStatsUpdateHandler.postDelayed(shaderStatsUpdater!!, 500) + } + } + shaderStatsUpdateHandler.post(shaderStatsUpdater!!) + } else { + if (shaderStatsUpdater != null) { + shaderStatsUpdateHandler.removeCallbacks(shaderStatsUpdater!!) + } + } + } + private fun getBatteryTemperature(context: Context): Float { return try { val batteryIntent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) @@ -706,6 +767,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() findItem(R.id.ram_meter).isChecked = BooleanSetting.SHOW_RAM_METER.getBoolean() + findItem(R.id.shader_building_overlay).isChecked = + BooleanSetting.SHOW_SHADER_BUILDING_OVERLAY.getBoolean() + findItem(R.id.performance_graph).isChecked = + BooleanSetting.SHOW_PERFORMANCE_GRAPH.getBoolean() findItem(R.id.menu_rel_stick_center).isChecked = BooleanSetting.JOYSTICK_REL_CENTER.getBoolean() findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean() @@ -739,6 +804,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } + R.id.shader_building_overlay -> { + it.isChecked = !it.isChecked + BooleanSetting.SHOW_SHADER_BUILDING_OVERLAY.setBoolean(it.isChecked) + updateShaderBuildingOverlay() + true + } + + R.id.performance_graph -> { + it.isChecked = !it.isChecked + BooleanSetting.SHOW_PERFORMANCE_GRAPH.setBoolean(it.isChecked) + updateShaderBuildingOverlay() + true + } + R.id.menu_edit_overlay -> { binding.drawerLayout.close() binding.surfaceInputOverlay.requestFocus() @@ -1084,5 +1163,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!) private val thermalStatsUpdateHandler = Handler(Looper.myLooper()!!) private val ramStatsUpdateHandler = Handler(Looper.myLooper()!!) + private val shaderStatsUpdateHandler = Handler(Looper.myLooper()!!) } } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/views/ShaderBuildingOverlayView.kt b/src/android/app/src/main/java/org/citron/citron_emu/views/ShaderBuildingOverlayView.kt new file mode 100644 index 000000000..6b832fd0e --- /dev/null +++ b/src/android/app/src/main/java/org/citron/citron_emu/views/ShaderBuildingOverlayView.kt @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.citron.citron_emu.views + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.util.Log +import android.view.View +import kotlin.math.* + +class ShaderBuildingOverlayView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#CC000000") // More opaque background + style = Paint.Style.FILL + } + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#FF9800") // Orange border for shader building + style = Paint.Style.STROKE + strokeWidth = 2f + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 18f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.LEFT + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 14f + typeface = Typeface.DEFAULT + textAlign = Paint.Align.LEFT + setShadowLayer(2f, 1f, 1f, Color.BLACK) + } + + private val graphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#4CAF50") // Green for good performance + style = Paint.Style.STROKE + strokeWidth = 3f + } + + private val graphFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#4CAF50") + style = Paint.Style.FILL + alpha = 60 + } + + private val graphBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#40000000") + style = Paint.Style.FILL + } + + private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#40FFFFFF") + style = Paint.Style.STROKE + strokeWidth = 1f + } + + private val backgroundRect = RectF() + private val graphRect = RectF() + + // Performance data + private var currentFps: Float = 0f + private var currentFrameTime: Float = 0f + private var emulationSpeed: Float = 0f + private var shadersBuilding: Int = 0 + + // Graph data + private val frameTimeHistory = mutableListOf() + private val maxHistorySize = 120 // 2 seconds at 60 FPS + private var minFrameTime: Float = 16.67f + private var maxFrameTime: Float = 16.67f + private var avgFrameTime: Float = 16.67f + + // Animation + private var animationProgress: Float = 0f + private var isAnimating: Boolean = false + + fun updatePerformanceStats(fps: Float, frameTime: Float, speed: Float, shaders: Int) { + try { + currentFps = fps + currentFrameTime = frameTime + emulationSpeed = speed + shadersBuilding = shaders + + // Update frame time history for graph + if (frameTime > 0f) { + frameTimeHistory.add(frameTime) + if (frameTimeHistory.size > maxHistorySize) { + frameTimeHistory.removeAt(0) + } + + // Update min/max/avg + if (frameTimeHistory.isNotEmpty()) { + minFrameTime = frameTimeHistory.minOrNull() ?: 16.67f + maxFrameTime = frameTimeHistory.maxOrNull() ?: 16.67f + avgFrameTime = frameTimeHistory.average().toFloat() + } + } + + // Start animation if shaders are building + if (shadersBuilding > 0 && !isAnimating) { + isAnimating = true + post(object : Runnable { + override fun run() { + if (isAnimating) { + animationProgress += 0.1f + if (animationProgress >= 1f) { + animationProgress = 0f + } + invalidate() + postDelayed(this, 50) // 20 FPS animation + } + } + }) + } else if (shadersBuilding == 0) { + isAnimating = false + } + + invalidate() + } catch (e: Exception) { + Log.e("ShaderBuildingOverlay", "Error updating performance stats", e) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = 280 + val desiredHeight = 140 + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize) + else -> desiredWidth + } + + val height = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + backgroundRect.set(4f, 4f, w - 4f, h - 4f) + graphRect.set(20f, 80f, w - 20f, h - 20f) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw background with rounded corners + canvas.drawRoundRect(backgroundRect, 12f, 12f, backgroundPaint) + canvas.drawRoundRect(backgroundRect, 12f, 12f, borderPaint) + + val padding = 16f + var yOffset = padding + 20f + val lineHeight = 22f + + // Draw title + textPaint.color = Color.WHITE + canvas.drawText("CITRON Performance", padding, yOffset, textPaint) + yOffset += lineHeight + + // Draw FPS + val fpsColor = when { + currentFps >= 55f -> Color.parseColor("#4CAF50") // Green + currentFps >= 45f -> Color.parseColor("#FF9800") // Orange + currentFps >= 30f -> Color.parseColor("#FF5722") // Red orange + else -> Color.parseColor("#F44336") // Red + } + textPaint.color = fpsColor + canvas.drawText("FPS: ${currentFps.roundToInt()}", padding, yOffset, textPaint) + yOffset += lineHeight - 4f + + // Draw frame time + smallTextPaint.color = fpsColor + canvas.drawText("Frame: ${String.format("%.1f", currentFrameTime)} ms", padding, yOffset, smallTextPaint) + yOffset += lineHeight - 4f + + // Draw emulation speed + canvas.drawText("Speed: ${emulationSpeed.roundToInt()}%", padding, yOffset, smallTextPaint) + yOffset += lineHeight - 4f + + // Draw shader building info with animation + if (shadersBuilding > 0) { + val shaderColor = Color.parseColor("#FF9800") // Orange + smallTextPaint.color = shaderColor + + // Animated dots + val dots = when ((animationProgress * 3).toInt()) { + 0 -> "Building: $shadersBuilding shader(s)" + 1 -> "Building: $shadersBuilding shader(s) ." + 2 -> "Building: $shadersBuilding shader(s) .." + else -> "Building: $shadersBuilding shader(s) ..." + } + canvas.drawText(dots, padding, yOffset, smallTextPaint) + } + + // Draw performance graph + drawPerformanceGraph(canvas) + } + + private fun drawPerformanceGraph(canvas: Canvas) { + if (frameTimeHistory.isEmpty()) return + + // Draw graph background + canvas.drawRoundRect(graphRect, 8f, 8f, graphBackgroundPaint) + + // Draw grid lines + val gridSpacing = graphRect.height() / 4 + for (i in 1..3) { + val y = graphRect.top + i * gridSpacing + canvas.drawLine(graphRect.left, y, graphRect.right, y, gridPaint) + } + + // Draw frame time line + val path = Path() + val pointSpacing = graphRect.width() / (frameTimeHistory.size - 1) + + for (i in frameTimeHistory.indices) { + val x = graphRect.left + i * pointSpacing + val normalizedFrameTime = (frameTimeHistory[i] - minFrameTime) / (maxFrameTime - minFrameTime) + val y = graphRect.bottom - (normalizedFrameTime * graphRect.height()) + + if (i == 0) { + path.moveTo(x, y) + } else { + path.lineTo(x, y) + } + } + + // Draw fill under the line + val fillPath = Path(path) + fillPath.lineTo(graphRect.right, graphRect.bottom) + fillPath.lineTo(graphRect.left, graphRect.bottom) + fillPath.close() + canvas.drawPath(fillPath, graphFillPaint) + + // Draw the line + canvas.drawPath(path, graphPaint) + + // Draw statistics + val statsText = "Min: ${String.format("%.1f", minFrameTime)}ms | " + + "Avg: ${String.format("%.1f", avgFrameTime)}ms | " + + "Max: ${String.format("%.1f", maxFrameTime)}ms" + smallTextPaint.color = Color.WHITE + canvas.drawText(statsText, graphRect.left, graphRect.bottom + 16f, smallTextPaint) + } +} diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 38a4f9ee9..72389da6c 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -72,6 +72,10 @@ struct Values { Settings::Category::Overlay}; Settings::Setting show_ram_meter{linkage, false, "show_ram_meter", Settings::Category::Overlay}; + Settings::Setting show_shader_building_overlay{linkage, true, "show_shader_building_overlay", + Settings::Category::Overlay}; + Settings::Setting show_performance_graph{linkage, false, "show_performance_graph", + Settings::Category::Overlay}; Settings::Setting show_input_overlay{linkage, true, "show_input_overlay", Settings::Category::Overlay}; Settings::Setting touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay}; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index eae276e96..620fc33d3 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -62,6 +62,7 @@ #include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_surface.h" +#include "video_core/shader_notify.h" #define jconst [[maybe_unused]] const auto #define jauto [[maybe_unused]] auto @@ -605,6 +606,13 @@ jdoubleArray Java_org_citron_citron_1emu_NativeLibrary_getPerfStats(JNIEnv* env, return j_stats; } +jint Java_org_citron_citron_1emu_NativeLibrary_getShadersBuilding(JNIEnv* env, jclass clazz) { + if (EmulationSession::GetInstance().IsRunning()) { + return EmulationSession::GetInstance().System().GPU().ShaderNotify().ShadersBuilding(); + } + return 0; +} + jstring Java_org_citron_citron_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) { if (Settings::IsNceEnabled()) { return Common::Android::ToJString(env, "NCE"); diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml index 04e06a30c..b9461f270 100644 --- a/src/android/app/src/main/res/layout/fragment_emulation.xml +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -177,6 +177,15 @@ android:focusable="false" android:visibility="gone" /> + + diff --git a/src/android/app/src/main/res/menu/menu_overlay_options.xml b/src/android/app/src/main/res/menu/menu_overlay_options.xml index d601421a7..511428996 100644 --- a/src/android/app/src/main/res/menu/menu_overlay_options.xml +++ b/src/android/app/src/main/res/menu/menu_overlay_options.xml @@ -16,6 +16,16 @@ android:title="@string/emulation_ram_meter" android:checkable="true" /> + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index da9a00b04..9937f8eb7 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -521,6 +521,8 @@ FPS counter Battery temperature RAM usage meter + Shader building overlay + Performance graph Toggle controls Relative stick center D-pad slide diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp index b2683fa24..5a5fbebf8 100644 --- a/src/video_core/renderer_opengl/gl_shader_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp @@ -281,6 +281,8 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading, size_t total{}; size_t built{}; bool has_loaded{}; + size_t total_compute{}; + size_t total_graphics{}; } state; const auto queue_work{[&](Common::UniqueFunction&& work) { @@ -306,6 +308,7 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading, } }); ++state.total; + ++state.total_compute; }}; const auto load_graphics{[&](std::ifstream& file, std::vector envs) { GraphicsPipelineKey key; @@ -327,11 +330,22 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading, } }); ++state.total; + ++state.total_graphics; }}; LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics); LOG_INFO(Render_OpenGL, "Total Pipeline Count: {}", state.total); + // Pre-reserve cache maps to reduce rehashing during load/build + { + std::scoped_lock lock{state.mutex}; + if (state.total_compute > 0) { + compute_cache.reserve(state.total_compute); + } + if (state.total_graphics > 0) { + graphics_cache.reserve(state.total_graphics); + } + } std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); state.has_loaded = true; @@ -391,18 +405,8 @@ GraphicsPipeline* ShaderCache::BuiltPipeline(GraphicsPipeline* pipeline) const n if (!use_asynchronous_shaders) { return pipeline; } - // If something is using depth, we can assume that games are not rendering anything which - // will be used one time. - if (maxwell3d->regs.zeta_enable) { - return nullptr; - } - // If games are using a small index count, we can assume these are full screen quads. - // Usually these shaders are only used once for building textures so we can assume they - // can't be built async - const auto& draw_state = maxwell3d->draw_manager->GetDrawState(); - if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) { - return pipeline; - } + // When asynchronous shaders are enabled, avoid blocking the main thread completely. + // Skip the draw until the pipeline is ready to prevent stutter. return nullptr; } @@ -587,7 +591,9 @@ std::unique_ptr ShaderCache::CreateComputePipeline( info.glasm_use_storage_buffers = num_storage_buffers <= device.GetMaxGLASMStorageBufferBlocks(); std::string code{}; + code.reserve(8 * 1024); // reduce reallocs for typical small-to-medium shaders std::vector code_spirv; + code_spirv.reserve(16 * 1024 / sizeof(u32)); switch (device.GetShaderBackend()) { case Settings::ShaderBackend::Glsl: code = EmitGLSL(profile, program); @@ -608,7 +614,8 @@ std::unique_ptr ShaderCache::CreateComputePipeline( } std::unique_ptr ShaderCache::CreateWorkers() const { - return std::make_unique(std::max(std::thread::hardware_concurrency(), 2U) - 1, + // Use all available logical threads to maximize build throughput. + return std::make_unique(std::max(std::thread::hardware_concurrency(), 2U), "GlShaderBuilder", [this] { return Context{emu_window}; }); } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 996e2bec9..61b2197d9 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -265,7 +265,7 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program size_t GetTotalPipelineWorkers() { const size_t max_core_threads = - std::max(static_cast(std::thread::hardware_concurrency()), 2ULL) - 1ULL; + std::max(static_cast(std::thread::hardware_concurrency()), 2ULL); #ifdef ANDROID // Leave at least a few cores free in android constexpr size_t free_cores = 3ULL; @@ -484,6 +484,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading size_t built{}; bool has_loaded{}; std::unique_ptr statistics; + size_t total_compute{}; + size_t total_graphics{}; } state; if (device.IsKhrPipelineExecutablePropertiesEnabled()) { @@ -506,6 +508,7 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading } }); ++state.total; + ++state.total_compute; }}; const auto load_graphics{[&](std::ifstream& file, std::vector envs) { GraphicsPipelineCacheKey key; @@ -543,12 +546,23 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading } }); ++state.total; + ++state.total_graphics; }}; VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute, load_graphics); LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total); + // Pre-reserve space in caches to reduce rehashing during async builds + { + std::scoped_lock lock{state.mutex}; + if (state.total_compute > 0) { + compute_cache.reserve(state.total_compute); + } + if (state.total_graphics > 0) { + graphics_cache.reserve(state.total_graphics); + } + } std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); state.has_loaded = true; @@ -589,18 +603,8 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const if (!use_asynchronous_shaders) { return pipeline; } - // If something is using depth, we can assume that games are not rendering anything which - // will be used one time. - if (maxwell3d->regs.zeta_enable) { - return nullptr; - } - // If games are using a small index count, we can assume these are full screen quads. - // Usually these shaders are only used once for building textures so we can assume they - // can't be built async - const auto& draw_state = maxwell3d->draw_manager->GetDrawState(); - if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) { - return pipeline; - } + // When asynchronous shaders are enabled, avoid blocking the main thread completely. + // Skip the draw until the pipeline is ready to prevent stutter. return nullptr; } @@ -673,7 +677,8 @@ std::unique_ptr PipelineCache::CreateGraphicsPipeline( const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage)}; ConvertLegacyToGeneric(program, runtime_info); - const std::vector code{EmitSPIRV(profile, runtime_info, program, binding)}; + std::vector code = EmitSPIRV(profile, runtime_info, program, binding); + code.reserve(std::max(code.size(), 16 * 1024 / sizeof(u32))); device.SaveShader(code); modules[stage_index] = BuildShader(device, code); if (device.HasDebuggingToolAttached()) { @@ -767,7 +772,8 @@ std::unique_ptr PipelineCache::CreateComputePipeline( } auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)}; - const std::vector code{EmitSPIRV(profile, program)}; + std::vector code = EmitSPIRV(profile, program); + code.reserve(std::max(code.size(), 16 * 1024 / sizeof(u32))); device.SaveShader(code); vk::ShaderModule spv_module{BuildShader(device, code)}; if (device.HasDebuggingToolAttached()) {