mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 19:13: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
|
external fun getPerfStats(): DoubleArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of shaders currently being built
|
||||||
|
*/
|
||||||
|
external fun getShadersBuilding(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current CPU backend.
|
* Returns the current CPU backend.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||||||
SHOW_INPUT_OVERLAY("show_input_overlay"),
|
SHOW_INPUT_OVERLAY("show_input_overlay"),
|
||||||
TOUCHSCREEN("touchscreen"),
|
TOUCHSCREEN("touchscreen"),
|
||||||
SHOW_THERMAL_OVERLAY("show_thermal_overlay"),
|
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 =
|
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||||
NativeConfig.getBoolean(key, needsGlobal)
|
NativeConfig.getBoolean(key, needsGlobal)
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
private var perfStatsUpdater: (() -> Unit)? = null
|
private var perfStatsUpdater: (() -> Unit)? = null
|
||||||
private var thermalStatsUpdater: (() -> Unit)? = null
|
private var thermalStatsUpdater: (() -> Unit)? = null
|
||||||
private var ramStatsUpdater: (() -> Unit)? = null
|
private var ramStatsUpdater: (() -> Unit)? = null
|
||||||
|
private var shaderStatsUpdater: (() -> Unit)? = null
|
||||||
|
|
||||||
private var _binding: FragmentEmulationBinding? = null
|
private var _binding: FragmentEmulationBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@@ -379,6 +380,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
updateShowFpsOverlay()
|
updateShowFpsOverlay()
|
||||||
updateThermalOverlay()
|
updateThermalOverlay()
|
||||||
updateRamMeterOverlay()
|
updateRamMeterOverlay()
|
||||||
|
updateShaderBuildingOverlay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
|
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
|
||||||
@@ -389,6 +391,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
ViewUtils.hideView(binding.fpsIndicatorView)
|
ViewUtils.hideView(binding.fpsIndicatorView)
|
||||||
ViewUtils.hideView(binding.thermalIndicatorView)
|
ViewUtils.hideView(binding.thermalIndicatorView)
|
||||||
ViewUtils.hideView(binding.ramMeterView)
|
ViewUtils.hideView(binding.ramMeterView)
|
||||||
|
ViewUtils.hideView(binding.shaderBuildingOverlayView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emulationViewModel.drawerOpen.collect(viewLifecycleOwner) {
|
emulationViewModel.drawerOpen.collect(viewLifecycleOwner) {
|
||||||
@@ -414,6 +417,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
if (perfStatsUpdater != null) {
|
if (perfStatsUpdater != null) {
|
||||||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
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)
|
emulationState.changeProgram(emulationViewModel.programChanged.value)
|
||||||
emulationViewModel.setProgramChanged(-1)
|
emulationViewModel.setProgramChanged(-1)
|
||||||
emulationViewModel.setEmulationStopped(false)
|
emulationViewModel.setEmulationStopped(false)
|
||||||
@@ -479,6 +491,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
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()
|
NativeLibrary.clearEmulationActivity()
|
||||||
super.onDetach()
|
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 {
|
private fun getBatteryTemperature(context: Context): Float {
|
||||||
return try {
|
return try {
|
||||||
val batteryIntent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
val batteryIntent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
@@ -706,6 +767,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()
|
BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()
|
||||||
findItem(R.id.ram_meter).isChecked =
|
findItem(R.id.ram_meter).isChecked =
|
||||||
BooleanSetting.SHOW_RAM_METER.getBoolean()
|
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 =
|
findItem(R.id.menu_rel_stick_center).isChecked =
|
||||||
BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()
|
BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()
|
||||||
findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
|
findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
|
||||||
@@ -739,6 +804,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
true
|
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 -> {
|
R.id.menu_edit_overlay -> {
|
||||||
binding.drawerLayout.close()
|
binding.drawerLayout.close()
|
||||||
binding.surfaceInputOverlay.requestFocus()
|
binding.surfaceInputOverlay.requestFocus()
|
||||||
@@ -1084,5 +1163,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
||||||
private val thermalStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
private val thermalStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
||||||
private val ramStatsUpdateHandler = 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::Category::Overlay};
|
||||||
Settings::Setting<bool> show_ram_meter{linkage, false, "show_ram_meter",
|
Settings::Setting<bool> show_ram_meter{linkage, false, "show_ram_meter",
|
||||||
Settings::Category::Overlay};
|
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::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
|
||||||
Settings::Category::Overlay};
|
Settings::Category::Overlay};
|
||||||
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", 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/renderer_vulkan/renderer_vulkan.h"
|
||||||
#include "video_core/vulkan_common/vulkan_instance.h"
|
#include "video_core/vulkan_common/vulkan_instance.h"
|
||||||
#include "video_core/vulkan_common/vulkan_surface.h"
|
#include "video_core/vulkan_common/vulkan_surface.h"
|
||||||
|
#include "video_core/shader_notify.h"
|
||||||
|
|
||||||
#define jconst [[maybe_unused]] const auto
|
#define jconst [[maybe_unused]] const auto
|
||||||
#define jauto [[maybe_unused]] auto
|
#define jauto [[maybe_unused]] auto
|
||||||
@@ -605,6 +606,13 @@ jdoubleArray Java_org_citron_citron_1emu_NativeLibrary_getPerfStats(JNIEnv* env,
|
|||||||
return j_stats;
|
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) {
|
jstring Java_org_citron_citron_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) {
|
||||||
if (Settings::IsNceEnabled()) {
|
if (Settings::IsNceEnabled()) {
|
||||||
return Common::Android::ToJString(env, "NCE");
|
return Common::Android::ToJString(env, "NCE");
|
||||||
|
|||||||
@@ -177,6 +177,15 @@
|
|||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
android:visibility="gone" />
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -16,6 +16,16 @@
|
|||||||
android:title="@string/emulation_ram_meter"
|
android:title="@string/emulation_ram_meter"
|
||||||
android:checkable="true" />
|
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
|
<item
|
||||||
android:id="@+id/menu_edit_overlay"
|
android:id="@+id/menu_edit_overlay"
|
||||||
android:title="@string/emulation_touch_overlay_edit" />
|
android:title="@string/emulation_touch_overlay_edit" />
|
||||||
|
|||||||
@@ -521,6 +521,8 @@
|
|||||||
<string name="emulation_fps_counter">FPS counter</string>
|
<string name="emulation_fps_counter">FPS counter</string>
|
||||||
<string name="emulation_thermal_indicator">Battery temperature</string>
|
<string name="emulation_thermal_indicator">Battery temperature</string>
|
||||||
<string name="emulation_ram_meter">RAM usage meter</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_toggle_controls">Toggle controls</string>
|
||||||
<string name="emulation_rel_stick_center">Relative stick center</string>
|
<string name="emulation_rel_stick_center">Relative stick center</string>
|
||||||
<string name="emulation_dpad_slide">D-pad slide</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 total{};
|
||||||
size_t built{};
|
size_t built{};
|
||||||
bool has_loaded{};
|
bool has_loaded{};
|
||||||
|
size_t total_compute{};
|
||||||
|
size_t total_graphics{};
|
||||||
} state;
|
} state;
|
||||||
|
|
||||||
const auto queue_work{[&](Common::UniqueFunction<void, Context*>&& work) {
|
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;
|
||||||
|
++state.total_compute;
|
||||||
}};
|
}};
|
||||||
const auto load_graphics{[&](std::ifstream& file, std::vector<FileEnvironment> envs) {
|
const auto load_graphics{[&](std::ifstream& file, std::vector<FileEnvironment> envs) {
|
||||||
GraphicsPipelineKey key;
|
GraphicsPipelineKey key;
|
||||||
@@ -327,11 +330,22 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
++state.total;
|
++state.total;
|
||||||
|
++state.total_graphics;
|
||||||
}};
|
}};
|
||||||
LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics);
|
LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics);
|
||||||
|
|
||||||
LOG_INFO(Render_OpenGL, "Total Pipeline Count: {}", state.total);
|
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};
|
std::unique_lock lock{state.mutex};
|
||||||
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
||||||
state.has_loaded = true;
|
state.has_loaded = true;
|
||||||
@@ -391,18 +405,8 @@ GraphicsPipeline* ShaderCache::BuiltPipeline(GraphicsPipeline* pipeline) const n
|
|||||||
if (!use_asynchronous_shaders) {
|
if (!use_asynchronous_shaders) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
// If something is using depth, we can assume that games are not rendering anything which
|
// When asynchronous shaders are enabled, avoid blocking the main thread completely.
|
||||||
// will be used one time.
|
// Skip the draw until the pipeline is ready to prevent stutter.
|
||||||
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;
|
|
||||||
}
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +591,9 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
|
|||||||
info.glasm_use_storage_buffers = num_storage_buffers <= device.GetMaxGLASMStorageBufferBlocks();
|
info.glasm_use_storage_buffers = num_storage_buffers <= device.GetMaxGLASMStorageBufferBlocks();
|
||||||
|
|
||||||
std::string code{};
|
std::string code{};
|
||||||
|
code.reserve(8 * 1024); // reduce reallocs for typical small-to-medium shaders
|
||||||
std::vector<u32> code_spirv;
|
std::vector<u32> code_spirv;
|
||||||
|
code_spirv.reserve(16 * 1024 / sizeof(u32));
|
||||||
switch (device.GetShaderBackend()) {
|
switch (device.GetShaderBackend()) {
|
||||||
case Settings::ShaderBackend::Glsl:
|
case Settings::ShaderBackend::Glsl:
|
||||||
code = EmitGLSL(profile, program);
|
code = EmitGLSL(profile, program);
|
||||||
@@ -608,7 +614,8 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
|
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",
|
"GlShaderBuilder",
|
||||||
[this] { return Context{emu_window}; });
|
[this] { return Context{emu_window}; });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
|
|||||||
|
|
||||||
size_t GetTotalPipelineWorkers() {
|
size_t GetTotalPipelineWorkers() {
|
||||||
const size_t max_core_threads =
|
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
|
#ifdef ANDROID
|
||||||
// Leave at least a few cores free in android
|
// Leave at least a few cores free in android
|
||||||
constexpr size_t free_cores = 3ULL;
|
constexpr size_t free_cores = 3ULL;
|
||||||
@@ -484,6 +484,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
|||||||
size_t built{};
|
size_t built{};
|
||||||
bool has_loaded{};
|
bool has_loaded{};
|
||||||
std::unique_ptr<PipelineStatistics> statistics;
|
std::unique_ptr<PipelineStatistics> statistics;
|
||||||
|
size_t total_compute{};
|
||||||
|
size_t total_graphics{};
|
||||||
} state;
|
} state;
|
||||||
|
|
||||||
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
||||||
@@ -506,6 +508,7 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
++state.total;
|
++state.total;
|
||||||
|
++state.total_compute;
|
||||||
}};
|
}};
|
||||||
const auto load_graphics{[&](std::ifstream& file, std::vector<FileEnvironment> envs) {
|
const auto load_graphics{[&](std::ifstream& file, std::vector<FileEnvironment> envs) {
|
||||||
GraphicsPipelineCacheKey key;
|
GraphicsPipelineCacheKey key;
|
||||||
@@ -543,12 +546,23 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
++state.total;
|
++state.total;
|
||||||
|
++state.total_graphics;
|
||||||
}};
|
}};
|
||||||
VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute,
|
VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute,
|
||||||
load_graphics);
|
load_graphics);
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total);
|
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};
|
std::unique_lock lock{state.mutex};
|
||||||
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
|
||||||
state.has_loaded = true;
|
state.has_loaded = true;
|
||||||
@@ -589,18 +603,8 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
|||||||
if (!use_asynchronous_shaders) {
|
if (!use_asynchronous_shaders) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
// If something is using depth, we can assume that games are not rendering anything which
|
// When asynchronous shaders are enabled, avoid blocking the main thread completely.
|
||||||
// will be used one time.
|
// Skip the draw until the pipeline is ready to prevent stutter.
|
||||||
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;
|
|
||||||
}
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,7 +677,8 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
|
|||||||
|
|
||||||
const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage)};
|
const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage)};
|
||||||
ConvertLegacyToGeneric(program, runtime_info);
|
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);
|
device.SaveShader(code);
|
||||||
modules[stage_index] = BuildShader(device, code);
|
modules[stage_index] = BuildShader(device, code);
|
||||||
if (device.HasDebuggingToolAttached()) {
|
if (device.HasDebuggingToolAttached()) {
|
||||||
@@ -767,7 +772,8 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
|
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);
|
device.SaveShader(code);
|
||||||
vk::ShaderModule spv_module{BuildShader(device, code)};
|
vk::ShaderModule spv_module{BuildShader(device, code)};
|
||||||
if (device.HasDebuggingToolAttached()) {
|
if (device.HasDebuggingToolAttached()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user