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 <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-08-17 15:48:06 +10:00
parent 85324599a6
commit 06f13f3cb1
11 changed files with 429 additions and 29 deletions

View File

@@ -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.
*/

View File

@@ -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)

View File

@@ -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()!!)
}
}

View File

@@ -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<Float>()
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)
}
}

View File

@@ -72,6 +72,10 @@ struct Values {
Settings::Category::Overlay};
Settings::Setting<bool> show_ram_meter{linkage, false, "show_ram_meter",
Settings::Category::Overlay};
Settings::Setting<bool> show_shader_building_overlay{linkage, true, "show_shader_building_overlay",
Settings::Category::Overlay};
Settings::Setting<bool> show_performance_graph{linkage, false, "show_performance_graph",
Settings::Category::Overlay};
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
Settings::Category::Overlay};
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay};

View File

@@ -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");

View File

@@ -177,6 +177,15 @@
android:focusable="false"
android:visibility="gone" />
<org.citron.citron_emu.views.ShaderBuildingOverlayView
android:id="@+id/shader_building_overlay_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
</LinearLayout>
</FrameLayout>

View File

@@ -16,6 +16,16 @@
android:title="@string/emulation_ram_meter"
android:checkable="true" />
<item
android:id="@+id/shader_building_overlay"
android:title="@string/emulation_shader_building_overlay"
android:checkable="true" />
<item
android:id="@+id/performance_graph"
android:title="@string/emulation_performance_graph"
android:checkable="true" />
<item
android:id="@+id/menu_edit_overlay"
android:title="@string/emulation_touch_overlay_edit" />

View File

@@ -521,6 +521,8 @@
<string name="emulation_fps_counter">FPS counter</string>
<string name="emulation_thermal_indicator">Battery temperature</string>
<string name="emulation_ram_meter">RAM usage meter</string>
<string name="emulation_shader_building_overlay">Shader building overlay</string>
<string name="emulation_performance_graph">Performance graph</string>
<string name="emulation_toggle_controls">Toggle controls</string>
<string name="emulation_rel_stick_center">Relative stick center</string>
<string name="emulation_dpad_slide">D-pad slide</string>

View File

@@ -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<void, Context*>&& 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<FileEnvironment> 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<ComputePipeline> 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<u32> 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<ComputePipeline> ShaderCache::CreateComputePipeline(
}
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1,
// Use all available logical threads to maximize build throughput.
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U),
"GlShaderBuilder",
[this] { return Context{emu_window}; });
}

View File

@@ -265,7 +265,7 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
size_t GetTotalPipelineWorkers() {
const size_t max_core_threads =
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
std::max<size_t>(static_cast<size_t>(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<PipelineStatistics> 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<FileEnvironment> 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<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage)};
ConvertLegacyToGeneric(program, runtime_info);
const std::vector<u32> code{EmitSPIRV(profile, runtime_info, program, binding)};
std::vector<u32> code = EmitSPIRV(profile, runtime_info, program, binding);
code.reserve(std::max<size_t>(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<ComputePipeline> PipelineCache::CreateComputePipeline(
}
auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
const std::vector<u32> code{EmitSPIRV(profile, program)};
std::vector<u32> code = EmitSPIRV(profile, program);
code.reserve(std::max<size_t>(code.size(), 16 * 1024 / sizeof(u32)));
device.SaveShader(code);
vk::ShaderModule spv_module{BuildShader(device, code)};
if (device.HasDebuggingToolAttached()) {