mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 11:03:56 +00:00
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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}; });
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user