android: video_core: Add support for disk shader cache. (#64)
This commit is contained in:
		@@ -42,6 +42,7 @@ object NativeLibrary {
 | 
			
		||||
    const val Player8Device = 7
 | 
			
		||||
    const val ConsoleDevice = 8
 | 
			
		||||
 | 
			
		||||
    @JvmField
 | 
			
		||||
    var sEmulationActivity = WeakReference<EmulationActivity?>(null)
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.disk_shader_cache
 | 
			
		||||
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment
 | 
			
		||||
 | 
			
		||||
object DiskShaderCacheProgress {
 | 
			
		||||
    val finishLock = Object()
 | 
			
		||||
    private lateinit var fragment: ShaderProgressDialogFragment
 | 
			
		||||
 | 
			
		||||
    private fun prepareDialog() {
 | 
			
		||||
        val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
 | 
			
		||||
        emulationActivity.runOnUiThread {
 | 
			
		||||
            fragment = ShaderProgressDialogFragment.newInstance(
 | 
			
		||||
                emulationActivity.getString(R.string.loading),
 | 
			
		||||
                emulationActivity.getString(R.string.preparing_shaders)
 | 
			
		||||
            )
 | 
			
		||||
            fragment.show(emulationActivity.supportFragmentManager, ShaderProgressDialogFragment.TAG)
 | 
			
		||||
        }
 | 
			
		||||
        synchronized(finishLock) { finishLock.wait() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @JvmStatic
 | 
			
		||||
    fun loadProgress(stage: Int, progress: Int, max: Int) {
 | 
			
		||||
        val emulationActivity = NativeLibrary.sEmulationActivity.get()
 | 
			
		||||
            ?: error("[DiskShaderCacheProgress] EmulationActivity not present")
 | 
			
		||||
 | 
			
		||||
        when (LoadCallbackStage.values()[stage]) {
 | 
			
		||||
            LoadCallbackStage.Prepare -> prepareDialog()
 | 
			
		||||
            LoadCallbackStage.Build -> fragment.onUpdateProgress(
 | 
			
		||||
                emulationActivity.getString(R.string.building_shaders),
 | 
			
		||||
                progress,
 | 
			
		||||
                max
 | 
			
		||||
            )
 | 
			
		||||
            LoadCallbackStage.Complete -> fragment.dismiss()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Equivalent to VideoCore::LoadCallbackStage
 | 
			
		||||
    enum class LoadCallbackStage {
 | 
			
		||||
        Prepare, Build, Complete
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.disk_shader_cache
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.LiveData
 | 
			
		||||
import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
 | 
			
		||||
class ShaderProgressViewModel : ViewModel() {
 | 
			
		||||
    private val _progress = MutableLiveData(0)
 | 
			
		||||
    val progress: LiveData<Int> get() = _progress
 | 
			
		||||
 | 
			
		||||
    private val _max = MutableLiveData(0)
 | 
			
		||||
    val max: LiveData<Int> get() = _max
 | 
			
		||||
 | 
			
		||||
    private val _message = MutableLiveData("")
 | 
			
		||||
    val message: LiveData<String> get() = _message
 | 
			
		||||
 | 
			
		||||
    fun setProgress(progress: Int) {
 | 
			
		||||
        _progress.postValue(progress)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setMax(max: Int) {
 | 
			
		||||
        _max.postValue(max)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setMessage(msg: String) {
 | 
			
		||||
        _message.postValue(msg)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,101 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.disk_shader_cache.ui
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import androidx.fragment.app.DialogFragment
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
 | 
			
		||||
import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
 | 
			
		||||
 | 
			
		||||
class ShaderProgressDialogFragment : DialogFragment() {
 | 
			
		||||
    private var _binding: DialogProgressBarBinding? = null
 | 
			
		||||
    private val binding get() = _binding!!
 | 
			
		||||
 | 
			
		||||
    private lateinit var alertDialog: AlertDialog
 | 
			
		||||
 | 
			
		||||
    private lateinit var shaderProgressViewModel: ShaderProgressViewModel
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
 | 
			
		||||
        _binding = DialogProgressBarBinding.inflate(layoutInflater)
 | 
			
		||||
        shaderProgressViewModel =
 | 
			
		||||
            ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
 | 
			
		||||
 | 
			
		||||
        val title = requireArguments().getString(TITLE)
 | 
			
		||||
        val message = requireArguments().getString(MESSAGE)
 | 
			
		||||
 | 
			
		||||
        isCancelable = false
 | 
			
		||||
        alertDialog = MaterialAlertDialogBuilder(requireActivity())
 | 
			
		||||
            .setView(binding.root)
 | 
			
		||||
            .setTitle(title)
 | 
			
		||||
            .setMessage(message)
 | 
			
		||||
            .create()
 | 
			
		||||
        return alertDialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
        savedInstanceState: Bundle?
 | 
			
		||||
    ): View {
 | 
			
		||||
        return binding.root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
 | 
			
		||||
            binding.progressBar.progress = progress
 | 
			
		||||
            setUpdateText()
 | 
			
		||||
        }
 | 
			
		||||
        shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
 | 
			
		||||
            binding.progressBar.max = max
 | 
			
		||||
            setUpdateText()
 | 
			
		||||
        }
 | 
			
		||||
        shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
 | 
			
		||||
            alertDialog.setMessage(msg)
 | 
			
		||||
        }
 | 
			
		||||
        synchronized(DiskShaderCacheProgress.finishLock) { DiskShaderCacheProgress.finishLock.notifyAll() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
        _binding = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onUpdateProgress(msg: String, progress: Int, max: Int) {
 | 
			
		||||
        shaderProgressViewModel.setProgress(progress)
 | 
			
		||||
        shaderProgressViewModel.setMax(max)
 | 
			
		||||
        shaderProgressViewModel.setMessage(msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setUpdateText() {
 | 
			
		||||
        binding.progressText.text = String.format(
 | 
			
		||||
            "%d/%d",
 | 
			
		||||
            shaderProgressViewModel.progress.value,
 | 
			
		||||
            shaderProgressViewModel.max.value
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "ProgressDialogFragment"
 | 
			
		||||
        const val TITLE = "title"
 | 
			
		||||
        const val MESSAGE = "message"
 | 
			
		||||
 | 
			
		||||
        fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
 | 
			
		||||
            val frag = ShaderProgressDialogFragment()
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putString(TITLE, title)
 | 
			
		||||
            args.putString(MESSAGE, message)
 | 
			
		||||
            frag.arguments = args
 | 
			
		||||
            return frag
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -196,6 +196,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
 | 
			
		||||
        val rendererResolution = rendererSection.getSetting(SettingsFile.KEY_RENDERER_RESOLUTION)
 | 
			
		||||
        val rendererAspectRatio =
 | 
			
		||||
            rendererSection.getSetting(SettingsFile.KEY_RENDERER_ASPECT_RATIO)
 | 
			
		||||
        val rendererUseDiskShaderCache =
 | 
			
		||||
            rendererSection.getSetting(SettingsFile.KEY_RENDERER_USE_DISK_SHADER_CACHE)
 | 
			
		||||
        val rendererForceMaxClocks =
 | 
			
		||||
            rendererSection.getSetting(SettingsFile.KEY_RENDERER_FORCE_MAX_CLOCK)
 | 
			
		||||
        val rendererAsynchronousShaders =
 | 
			
		||||
@@ -250,6 +252,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
 | 
			
		||||
                    0
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    SettingsFile.KEY_RENDERER_USE_DISK_SHADER_CACHE,
 | 
			
		||||
                    Settings.SECTION_RENDERER,
 | 
			
		||||
                    rendererUseDiskShaderCache,
 | 
			
		||||
                    R.string.use_disk_shader_cache,
 | 
			
		||||
                    R.string.use_disk_shader_cache_description,
 | 
			
		||||
                    true
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    SettingsFile.KEY_RENDERER_FORCE_MAX_CLOCK,
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ object SettingsFile {
 | 
			
		||||
    const val KEY_RENDERER_RESOLUTION = "resolution_setup"
 | 
			
		||||
    const val KEY_RENDERER_ASPECT_RATIO = "aspect_ratio"
 | 
			
		||||
    const val KEY_RENDERER_ACCURACY = "gpu_accuracy"
 | 
			
		||||
    const val KEY_RENDERER_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"
 | 
			
		||||
    const val KEY_RENDERER_ASYNCHRONOUS_SHADERS = "use_asynchronous_shaders"
 | 
			
		||||
    const val KEY_RENDERER_FORCE_MAX_CLOCK = "force_max_clock"
 | 
			
		||||
    const val KEY_RENDERER_USE_SPEED_LIMIT = "use_speed_limit"
 | 
			
		||||
 
 | 
			
		||||
@@ -226,10 +226,6 @@ void Config::ReadValues() {
 | 
			
		||||
    ReadSetting("Renderer", Settings::values.bg_green);
 | 
			
		||||
    ReadSetting("Renderer", Settings::values.bg_blue);
 | 
			
		||||
 | 
			
		||||
    // Disable shader cache by default on Android
 | 
			
		||||
    Settings::values.use_disk_shader_cache =
 | 
			
		||||
        config->GetBoolean("Renderer", "use_disk_shader_cache", false);
 | 
			
		||||
 | 
			
		||||
    // Enable force_max_clock by default on Android
 | 
			
		||||
    Settings::values.renderer_force_max_clock =
 | 
			
		||||
        config->GetBoolean("Renderer", "force_max_clock", true);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,18 @@
 | 
			
		||||
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "common/assert.h"
 | 
			
		||||
#include "common/fs/fs_android.h"
 | 
			
		||||
#include "jni/applets/software_keyboard.h"
 | 
			
		||||
#include "jni/id_cache.h"
 | 
			
		||||
#include "video_core/rasterizer_interface.h"
 | 
			
		||||
 | 
			
		||||
static JavaVM* s_java_vm;
 | 
			
		||||
static jclass s_native_library_class;
 | 
			
		||||
static jclass s_disk_cache_progress_class;
 | 
			
		||||
static jclass s_load_callback_stage_class;
 | 
			
		||||
static jmethodID s_exit_emulation_activity;
 | 
			
		||||
static jmethodID s_disk_cache_load_progress;
 | 
			
		||||
 | 
			
		||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
 | 
			
		||||
 | 
			
		||||
@@ -38,10 +43,22 @@ jclass GetNativeLibraryClass() {
 | 
			
		||||
    return s_native_library_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetDiskCacheProgressClass() {
 | 
			
		||||
    return s_disk_cache_progress_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetDiskCacheLoadCallbackStageClass() {
 | 
			
		||||
    return s_load_callback_stage_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetExitEmulationActivity() {
 | 
			
		||||
    return s_exit_emulation_activity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetDiskCacheLoadProgress() {
 | 
			
		||||
    return s_disk_cache_load_progress;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace IDCache
 | 
			
		||||
 | 
			
		||||
#ifdef __cplusplus
 | 
			
		||||
@@ -58,8 +75,16 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
 | 
			
		||||
    // Initialize Java classes
 | 
			
		||||
    const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
 | 
			
		||||
    s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
 | 
			
		||||
    s_disk_cache_progress_class = reinterpret_cast<jclass>(env->NewGlobalRef(
 | 
			
		||||
        env->FindClass("org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress")));
 | 
			
		||||
    s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
 | 
			
		||||
        "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
 | 
			
		||||
 | 
			
		||||
    // Initialize methods
 | 
			
		||||
    s_exit_emulation_activity =
 | 
			
		||||
        env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
 | 
			
		||||
    s_disk_cache_load_progress =
 | 
			
		||||
        env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
 | 
			
		||||
 | 
			
		||||
    // Initialize Android Storage
 | 
			
		||||
    Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
 | 
			
		||||
@@ -79,6 +104,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
 | 
			
		||||
    // UnInitialize Android Storage
 | 
			
		||||
    Common::FS::Android::UnRegisterCallbacks();
 | 
			
		||||
    env->DeleteGlobalRef(s_native_library_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_disk_cache_progress_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_load_callback_stage_class);
 | 
			
		||||
 | 
			
		||||
    // UnInitialze applets
 | 
			
		||||
    SoftwareKeyboard::CleanupJNI(env);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,15 @@
 | 
			
		||||
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "video_core/rasterizer_interface.h"
 | 
			
		||||
 | 
			
		||||
namespace IDCache {
 | 
			
		||||
 | 
			
		||||
JNIEnv* GetEnvForThread();
 | 
			
		||||
jclass GetNativeLibraryClass();
 | 
			
		||||
jclass GetDiskCacheProgressClass();
 | 
			
		||||
jclass GetDiskCacheLoadCallbackStageClass();
 | 
			
		||||
jmethodID GetExitEmulationActivity();
 | 
			
		||||
jmethodID GetDiskCacheLoadProgress();
 | 
			
		||||
 | 
			
		||||
} // namespace IDCache
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@
 | 
			
		||||
#include "jni/emu_window/emu_window.h"
 | 
			
		||||
#include "jni/id_cache.h"
 | 
			
		||||
#include "video_core/rasterizer_interface.h"
 | 
			
		||||
#include "video_core/renderer_base.h"
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -229,6 +230,15 @@ public:
 | 
			
		||||
            m_is_running = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Load the disk shader cache.
 | 
			
		||||
        if (Settings::values.use_disk_shader_cache.GetValue()) {
 | 
			
		||||
            LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
 | 
			
		||||
            m_system.Renderer().ReadRasterizer()->LoadDiskResources(
 | 
			
		||||
                m_system.GetApplicationProcessProgramID(), std::stop_token{},
 | 
			
		||||
                LoadDiskCacheProgress);
 | 
			
		||||
            LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void(m_system.Run());
 | 
			
		||||
 | 
			
		||||
        if (m_system.DebuggerEnabled()) {
 | 
			
		||||
@@ -295,6 +305,14 @@ private:
 | 
			
		||||
        return entry;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
 | 
			
		||||
        JNIEnv* env = IDCache::GetEnvForThread();
 | 
			
		||||
        env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
 | 
			
		||||
                                  IDCache::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
 | 
			
		||||
                                  static_cast<jint>(progress), static_cast<jint>(max));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static EmulationSession s_instance;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,4 +12,13 @@
 | 
			
		||||
        android:layout_margin="24dp"
 | 
			
		||||
        app:trackCornerRadius="4dp" />
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/progress_text"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginLeft="24dp"
 | 
			
		||||
        android:layout_marginRight="24dp"
 | 
			
		||||
        android:layout_marginBottom="24dp"
 | 
			
		||||
        android:gravity="end" />
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,8 @@
 | 
			
		||||
    <string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously, which will reduce stutter but may introduce glitches.</string>
 | 
			
		||||
    <string name="renderer_debug">Enable graphics debugging</string>
 | 
			
		||||
    <string name="renderer_debug_description">When checked, the graphics API enters a slower debugging mode.</string>
 | 
			
		||||
    <string name="use_disk_shader_cache">Use disk shader cache</string>
 | 
			
		||||
    <string name="use_disk_shader_cache_description">Reduce stuttering by storing and loading generated shaders to disk.</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Audio settings strings -->
 | 
			
		||||
    <string name="audio_volume">Volume</string>
 | 
			
		||||
@@ -45,6 +47,7 @@
 | 
			
		||||
    <string name="ini_saved">Saved settings</string>
 | 
			
		||||
    <string name="gameid_saved">Saved settings for %1$s</string>
 | 
			
		||||
    <string name="error_saving">Error saving %1$s.ini: %2$s</string>
 | 
			
		||||
    <string name="loading">Loading...</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Game Grid Screen-->
 | 
			
		||||
    <string name="grid_menu_core_settings">Settings</string>
 | 
			
		||||
@@ -183,4 +186,8 @@
 | 
			
		||||
    <string name="gamepad_home">Home</string>
 | 
			
		||||
    <string name="gamepad_screenshot">Screenshot</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Disk shader cache -->
 | 
			
		||||
    <string name="preparing_shaders">Preparing shaders</string>
 | 
			
		||||
    <string name="building_shaders">Building shaders</string>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user