diff --git a/app/build.gradle b/app/build.gradle index c0f01108..24deda51 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,6 +189,7 @@ dependencies { /* Other Java */ implementation 'info.debatty:java-string-similarity:2.0.0' implementation 'com.github.KikiManjaro:colorpicker:v1.1.12' + implementation 'com.github.android:renderscript-intrinsics-replacement-toolkit:344be3f' } kapt { diff --git a/app/src/main/cpp/skyline/gpu.cpp b/app/src/main/cpp/skyline/gpu.cpp index 84e0d765..4d55cc2d 100644 --- a/app/src/main/cpp/skyline/gpu.cpp +++ b/app/src/main/cpp/skyline/gpu.cpp @@ -414,6 +414,6 @@ namespace skyline::gpu { if (!*state.settings->disableShaderCache) graphicsPipelineCacheManager.emplace(state, state.os->publicAppFilesPath + "graphics_pipeline_cache/" + titleId); - graphicsPipelineManager.emplace(*this); + graphicsPipelineManager.emplace(*this, *state.jvm); } } diff --git a/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.cpp b/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.cpp index d8cb6222..7cf14e2c 100644 --- a/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.cpp +++ b/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.cpp @@ -214,6 +214,9 @@ namespace skyline::gpu { std::scoped_lock lock{mutex}; compilePendingDescs.erase(pipelineDescIt); + if (compilationCallback) + compilationCallback(); + return pipeline; } @@ -252,4 +255,15 @@ namespace skyline::gpu { SerialisePipelineCache(gpu, pipelineCacheDir, rawData); }); } + + void GraphicsPipelineAssembler::RegisterCompilationCallback(std::function callback) { + if (compilationCallback) + throw exception("A compilation callback is already registered"); + + compilationCallback = std::move(callback); + } + + void GraphicsPipelineAssembler::UnregisterCompilationCallback() { + compilationCallback = {}; + } } diff --git a/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.h b/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.h index 2d5c9a9e..4408ffa5 100644 --- a/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.h +++ b/app/src/main/cpp/skyline/gpu/graphics_pipeline_assembler.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -59,6 +60,7 @@ namespace skyline::gpu { vk::raii::PipelineCache vkPipelineCache; //!< A Vulkan Pipeline Cache which stores all unique graphics pipelines BS::thread_pool pool; std::string pipelineCacheDir; + std::function compilationCallback; /** * @brief All unique metadata in a single attachment for a compatible render pass according to Render Pass Compatibility clause in the Vulkan specification @@ -158,5 +160,15 @@ namespace skyline::gpu { * @brief Saves the current Vulkan pipeline cache to the filesystem */ void SavePipelineCache(); + + /** + * @brief Registers a callback that is called whenever a pipeline is compiled + */ + void RegisterCompilationCallback(std::function callback); + + /** + * @brief Unregisters the compilation callback + */ + void UnregisterCompilationCallback(); }; } diff --git a/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.cpp b/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.cpp index 3bb78d57..a8002ea4 100644 --- a/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.cpp +++ b/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "graphics_pipeline_state_accessor.h" #include "pipeline_manager.h" @@ -942,12 +943,19 @@ namespace skyline::gpu::interconnect::maxwell3d { }); } - PipelineManager::PipelineManager(GPU &gpu) { + PipelineManager::PipelineManager(GPU &gpu, JvmManager &jvm) { if (!gpu.graphicsPipelineCacheManager) return; - std::ifstream stream{gpu.graphicsPipelineCacheManager->OpenReadStream()}; + std::atomic compiledCount{}; + auto [stream, totalPipelineCount]{gpu.graphicsPipelineCacheManager->OpenReadStream()}; i64 lastKnownGoodOffset{stream.tellg()}; + + jvm.ShowPipelineLoadingScreen(totalPipelineCount); + gpu.graphicsPipelineAssembler->RegisterCompilationCallback([&]() { + jvm.UpdatePipelineLoadingProgress(++compiledCount); + }); + try { auto startTime{util::GetTimeNs()}; PipelineStateBundle bundle; @@ -986,8 +994,10 @@ namespace skyline::gpu::interconnect::maxwell3d { } catch (const exception &e) { Logger::Warn("Pipeline cache corrupted at: 0x{:X}, error: {}", lastKnownGoodOffset, e.what()); gpu.graphicsPipelineCacheManager->InvalidateAllAfter(static_cast(lastKnownGoodOffset)); - return; } + + gpu.graphicsPipelineAssembler->UnregisterCompilationCallback(); + jvm.HidePipelineLoadingScreen(); } Pipeline *PipelineManager::FindOrCreate(InterconnectContext &ctx, Textures &textures, ConstantBufferSet &constantBuffers, const PackedPipelineState &packedState, const std::array &shaderBinaries) { diff --git a/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.h b/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.h index fdc95e04..ed612fe7 100644 --- a/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.h +++ b/app/src/main/cpp/skyline/gpu/interconnect/maxwell_3d/pipeline_manager.h @@ -272,7 +272,7 @@ namespace skyline::gpu::interconnect::maxwell3d { #endif public: - PipelineManager(GPU &gpu); + PipelineManager(GPU &gpu, JvmManager &jvm); Pipeline *FindOrCreate(InterconnectContext &ctx, Textures &textures, ConstantBufferSet &constantBuffers, const PackedPipelineState &packedState, const std::array &shaderBinaries); }; diff --git a/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.cpp b/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.cpp index 87b1f13b..41f27367 100644 --- a/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.cpp +++ b/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.cpp @@ -8,19 +8,26 @@ namespace skyline::gpu { struct PipelineCacheFileHeader { static constexpr u32 Magic{util::MakeMagic("PCHE")}; //!< The magic value used to identify a pipeline cache file - static constexpr u32 Version{2}; //!< The version of the pipeline cache file format, MUST be incremented for any format changes + static constexpr u32 Version{3}; //!< The version of the pipeline cache file format, MUST be incremented for any format changes u32 magic{Magic}; u32 version{Version}; + u32 count{0}; //!< The total number of pipeline cache bundles in the file auto operator<=>(const PipelineCacheFileHeader &) const = default; - }; - static constexpr PipelineCacheFileHeader ValidPipelineCacheFileHeader{}; + /** + * @brief Checks if the header is valid + */ + bool IsValid() { + return magic == Magic && version == Version; + } + }; void PipelineCacheManager::Run() { std::ofstream stream{stagingPath, std::ios::binary | std::ios::trunc}; - stream.write(reinterpret_cast(&ValidPipelineCacheFileHeader), sizeof(PipelineCacheFileHeader)); + PipelineCacheFileHeader header{}; + stream.write(reinterpret_cast(&header), sizeof(PipelineCacheFileHeader)); while (true) { std::unique_lock lock(writeMutex); @@ -31,7 +38,15 @@ namespace skyline::gpu { auto bundle{std::move(writeQueue.front())}; writeQueue.pop(); lock.unlock(); + bundle->Serialise(stream); + + header.count++; + // Rewrite the header with the updated count + auto savedPosition{stream.tellp()}; + stream.seekp(0, std::ios_base::beg); + stream.write(reinterpret_cast(&header), sizeof(PipelineCacheFileHeader)); + stream.seekp(savedPosition); } } @@ -40,8 +55,8 @@ namespace skyline::gpu { return false; PipelineCacheFileHeader header{}; - stream.read(reinterpret_cast(&header), sizeof(header)); - return header == ValidPipelineCacheFileHeader; + stream.read(reinterpret_cast(&header), sizeof(PipelineCacheFileHeader)); + return header.IsValid(); } void PipelineCacheManager::MergeStaging() { @@ -49,12 +64,24 @@ namespace skyline::gpu { if (stagingStream.fail()) return; // If the staging file doesn't exist then there's nothing to merge - if (!ValidateHeader(stagingStream)) { + PipelineCacheFileHeader stagingHeader{}; + stagingStream.read(reinterpret_cast(&stagingHeader), sizeof(PipelineCacheFileHeader)); + if (!stagingHeader.IsValid()) { Logger::Warn("Discarding invalid pipeline cache staging file"); return; } - std::ofstream mainStream{mainPath, std::ios::binary | std::ios::app}; + std::fstream mainStream{mainPath, std::ios::binary | std::ios::in | std::ios::out}; + PipelineCacheFileHeader mainHeader{}; + mainStream.seekg(0, std::ios_base::beg); + mainStream.read(reinterpret_cast(&mainHeader), sizeof(PipelineCacheFileHeader)); + + // Update the main header with the new count + mainHeader.count += stagingHeader.count; + mainStream.seekp(0, std::ios_base::beg); + mainStream.write(reinterpret_cast(&mainHeader), sizeof(PipelineCacheFileHeader)); + + mainStream.seekp(0, std::ios::end); mainStream << stagingStream.rdbuf(); } @@ -73,7 +100,8 @@ namespace skyline::gpu { if (!didExist) { // If the main file didn't exist we need to write the header std::filesystem::create_directories(std::filesystem::path{mainPath}.parent_path()); std::ofstream mainStream{mainPath, std::ios::binary | std::ios::app}; - mainStream.write(reinterpret_cast(&ValidPipelineCacheFileHeader), sizeof(PipelineCacheFileHeader)); + PipelineCacheFileHeader header{}; + mainStream.write(reinterpret_cast(&header), sizeof(PipelineCacheFileHeader)); } // Merge any staging changes into the main file before starting the writer thread @@ -87,12 +115,17 @@ namespace skyline::gpu { writeCondition.notify_one(); } - std::ifstream PipelineCacheManager::OpenReadStream() { + std::pair PipelineCacheManager::OpenReadStream() { auto mainStream{std::ifstream{mainPath, std::ios::binary}}; - if (!ValidateHeader(mainStream)) + if (mainStream.fail()) + throw exception("Pipeline cache main file missing at runtime!"); + + PipelineCacheFileHeader header{}; + mainStream.read(reinterpret_cast(&header), sizeof(PipelineCacheFileHeader)); + if (!header.IsValid()) throw exception("Pipeline cache main file corrupted at runtime!"); - return mainStream; + return {std::move(mainStream), header.count}; } void PipelineCacheManager::InvalidateAllAfter(u64 offset) { diff --git a/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.h b/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.h index 21b22713..62e97583 100644 --- a/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.h +++ b/app/src/main/cpp/skyline/gpu/pipeline_cache_manager.h @@ -34,7 +34,11 @@ namespace skyline::gpu { */ void QueueWrite(std::unique_ptr bundle); - std::ifstream OpenReadStream(); + /** + * @brief Opens the main pipeline cache file for reading + * @return A pair containing the stream and the total pipeline count + */ + std::pair OpenReadStream(); /** * @brief Shrinks the pipeline cache file to `offset` bytes, removing any (potentially invalid) data after that point diff --git a/app/src/main/cpp/skyline/jvm.cpp b/app/src/main/cpp/skyline/jvm.cpp index 5af9c305..e41f5caa 100644 --- a/app/src/main/cpp/skyline/jvm.cpp +++ b/app/src/main/cpp/skyline/jvm.cpp @@ -63,8 +63,11 @@ namespace skyline { waitForSubmitOrCancelId{environ->GetMethodID(instanceClass, "waitForSubmitOrCancel", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)[Ljava/lang/Object;")}, closeKeyboardId{environ->GetMethodID(instanceClass, "closeKeyboard", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)V")}, showValidationResultId{environ->GetMethodID(instanceClass, "showValidationResult", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;ILjava/lang/String;)I")}, - getVersionCodeId{environ->GetMethodID(instanceClass, "getVersionCode", "()I")}, - getIntegerValueId{environ->GetMethodID(environ->FindClass("java/lang/Integer"), "intValue", "()I")} { + getIntegerValueId{environ->GetMethodID(environ->FindClass("java/lang/Integer"), "intValue", "()I")}, + showPipelineLoadingScreenId{environ->GetMethodID(instanceClass, "showPipelineLoadingScreen", "(I)V")}, + updatePipelineLoadingProgressId{environ->GetMethodID(instanceClass, "updatePipelineLoadingProgress", "(I)V")}, + hidePipelineLoadingScreenId{environ->GetMethodID(instanceClass, "hidePipelineLoadingScreen", "()V")}, + getVersionCodeId{environ->GetMethodID(instanceClass, "getVersionCode", "()I")} { env.Initialize(environ); } @@ -137,14 +140,26 @@ namespace skyline { env->DeleteGlobalRef(dialog); } - i32 JvmManager::GetVersionCode() { - return env->CallIntMethod(instance, getVersionCodeId); - } - JvmManager::KeyboardCloseResult JvmManager::ShowValidationResult(jobject dialog, KeyboardTextCheckResult checkResult, std::u16string message) { auto str{env->NewString(reinterpret_cast(message.data()), static_cast(message.length()))}; auto result{static_cast(env->CallIntMethod(instance, showValidationResultId, dialog, checkResult, str))}; env->DeleteLocalRef(str); return result; } + + void JvmManager::ShowPipelineLoadingScreen(u32 totalPipelineCount) { + env->CallVoidMethod(instance, showPipelineLoadingScreenId, static_cast(totalPipelineCount)); + } + + void JvmManager::UpdatePipelineLoadingProgress(u32 progress) { + env->CallVoidMethod(instance, updatePipelineLoadingProgressId, static_cast(progress)); + } + + void JvmManager::HidePipelineLoadingScreen() { + env->CallVoidMethod(instance, hidePipelineLoadingScreenId); + } + + i32 JvmManager::GetVersionCode() { + return env->CallIntMethod(instance, getVersionCodeId); + } } diff --git a/app/src/main/cpp/skyline/jvm.h b/app/src/main/cpp/skyline/jvm.h index 535a307a..c5e271d6 100644 --- a/app/src/main/cpp/skyline/jvm.h +++ b/app/src/main/cpp/skyline/jvm.h @@ -176,6 +176,21 @@ namespace skyline { */ KeyboardCloseResult ShowValidationResult(KeyboardHandle dialog, KeyboardTextCheckResult checkResult, std::u16string message); + /** + * @brief A call to EmulationActivity.showPipelineLoadingScreen in Kotlin + */ + void ShowPipelineLoadingScreen(u32 totalPipelineCount); + + /** + * @brief A call to EmulationActivity.updatePipelineLoadingProgress in Kotlin + */ + void UpdatePipelineLoadingProgress(u32 progress); + + /** + * @brief A call to EmulationActivity.hidePipelineLoadingScreen in Kotlin + */ + void HidePipelineLoadingScreen(); + /** * @brief A call to EmulationActivity.getVersionCode in Kotlin * @return A version code in Vulkan's format with 14-bit patch + 10-bit major and minor components @@ -186,12 +201,17 @@ namespace skyline { jmethodID initializeControllersId; jmethodID vibrateDeviceId; jmethodID clearVibrationDeviceId; + jmethodID showKeyboardId; jmethodID waitForSubmitOrCancelId; jmethodID closeKeyboardId; jmethodID showValidationResultId; - jmethodID getVersionCodeId; - jmethodID getIntegerValueId; + + jmethodID showPipelineLoadingScreenId; + jmethodID updatePipelineLoadingProgressId; + jmethodID hidePipelineLoadingScreenId; + + jmethodID getVersionCodeId; }; } diff --git a/app/src/main/java/emu/skyline/EmulationActivity.kt b/app/src/main/java/emu/skyline/EmulationActivity.kt index 41a42742..a0c71c48 100644 --- a/app/src/main/java/emu/skyline/EmulationActivity.kt +++ b/app/src/main/java/emu/skyline/EmulationActivity.kt @@ -32,12 +32,12 @@ import androidx.core.view.updateMargins import androidx.fragment.app.FragmentTransaction import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint -import emu.skyline.BuildConfig import emu.skyline.applet.swkbd.SoftwareKeyboardConfig import emu.skyline.applet.swkbd.SoftwareKeyboardDialog import emu.skyline.data.AppItem import emu.skyline.data.AppItemTag import emu.skyline.databinding.EmuActivityBinding +import emu.skyline.emulation.PipelineLoadingFragment import emu.skyline.input.* import emu.skyline.loader.RomFile import emu.skyline.loader.getRomFormat @@ -447,7 +447,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo } } - override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { + override fun onPictureInPictureModeChanged(isInPictureInPictureMode : Boolean, newConfig : Configuration) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) if (isInPictureInPictureMode) { @@ -464,10 +464,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo } else { try { unregisterReceiver(pictureInPictureReceiver) - } catch (ignored : Exception) { } + } catch (ignored : Exception) { + } resumeEmulator() - + binding.onScreenControllerView.apply { controllerType = inputHandler.getFirstControllerType() isGone = controllerType == ControllerType.None || !appSettings.onScreenControl @@ -512,6 +513,44 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo vibrators.clear() } + private lateinit var pipelineLoadingFragment : PipelineLoadingFragment + + /** + * Called by native code to show the pipeline loading progress screen + */ + @Suppress("unused") + fun showPipelineLoadingScreen(totalPipelineCount : Int) { + pipelineLoadingFragment = PipelineLoadingFragment.newInstance(item, totalPipelineCount) + + supportFragmentManager + .beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) + .replace(R.id.emulation_fragment, pipelineLoadingFragment) + .commit() + } + + /** + * Called by native code to update the pipeline loading progress + */ + @Suppress("unused") + fun updatePipelineLoadingProgress(progress : Int) { + runOnUiThread { + pipelineLoadingFragment.updateProgress(progress) + } + } + + /** + * Called by native code to hide the pipeline loading progress screen + */ + @Suppress("unused") + fun hidePipelineLoadingScreen() { + supportFragmentManager + .beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) + .remove(pipelineLoadingFragment) + .commit() + } + override fun surfaceCreated(holder : SurfaceHolder) { Log.d(Tag, "surfaceCreated Holder: $holder") diff --git a/app/src/main/java/emu/skyline/emulation/PipelineLoadingFragment.kt b/app/src/main/java/emu/skyline/emulation/PipelineLoadingFragment.kt new file mode 100644 index 00000000..0c8f3f51 --- /dev/null +++ b/app/src/main/java/emu/skyline/emulation/PipelineLoadingFragment.kt @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.emulation + +import android.annotation.SuppressLint +import android.graphics.RenderEffect +import android.graphics.Shader +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.google.android.renderscript.Toolkit +import emu.skyline.data.AppItem +import emu.skyline.data.AppItemTag +import emu.skyline.databinding.PipelineLoadingBinding +import emu.skyline.utils.serializable + +private const val TotalPipelineCountTag = "PipelineLoadingFragment::TotalCount" +private const val PipelineProgressTag = "PipelineLoadingFragment::Progress" + +class PipelineLoadingFragment : Fragment() { + private val item by lazy { requireArguments().serializable(AppItemTag)!! } + private val totalPipelineCount by lazy { requireArguments().getInt(TotalPipelineCountTag) } + + private lateinit var binding : PipelineLoadingBinding + + override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = PipelineLoadingBinding.inflate(inflater).also { binding = it }.root + + @SuppressLint("SetTextI18n") + override fun onViewCreated(view : View, savedInstanceState : Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.gameTitle.apply { + text = item.title + isSelected = true + } + binding.gameVersion.text = item.version + binding.gameIcon.setImageBitmap(item.bitmapIcon) + + val progress = savedInstanceState?.getInt(PipelineProgressTag) ?: 0 + binding.progressBar.max = totalPipelineCount + updateProgress(progress) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + binding.backgroundImage.setImageBitmap(item.bitmapIcon) + binding.backgroundImage.setRenderEffect(RenderEffect.createBlurEffect(75f, 75f, Shader.TileMode.MIRROR)) + } else { + binding.backgroundImage.setImageBitmap(Toolkit.blur(item.bitmapIcon, 15)) + } + } + + override fun onSaveInstanceState(outState : Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(PipelineProgressTag, binding.progressBar.progress) + } + + @SuppressLint("SetTextI18n") + fun updateProgress(progress : Int) { + if (!this::binding.isInitialized) + return + + binding.progressBar.progress = progress + binding.progressLabel.text = "$progress/$totalPipelineCount (${(progress.toFloat() / totalPipelineCount * 100).toInt()}%)" + } + + companion object { + fun newInstance(item : AppItem, totalPipelineCount : Int) = PipelineLoadingFragment().apply { + arguments = Bundle().apply { + putSerializable(AppItemTag, item) + putInt(TotalPipelineCountTag, totalPipelineCount) + } + } + } +} diff --git a/app/src/main/res/layout/emu_activity.xml b/app/src/main/res/layout/emu_activity.xml index 47db2aec..34809649 100644 --- a/app/src/main/res/layout/emu_activity.xml +++ b/app/src/main/res/layout/emu_activity.xml @@ -27,8 +27,8 @@ android:layout_gravity="top|left" android:layout_marginLeft="@dimen/onScreenItemHorizontalMargin" android:layout_marginTop="5dp" - tools:text="60 FPS\n16.6±0.10ms" - android:textColor="@color/colorPerfStatsPrimary" /> + android:textColor="@color/colorPerfStatsPrimary" + tools:text="60 FPS\n16.6±0.10ms" /> + + diff --git a/app/src/main/res/layout/shader_loading.xml b/app/src/main/res/layout/pipeline_loading.xml similarity index 94% rename from app/src/main/res/layout/shader_loading.xml rename to app/src/main/res/layout/pipeline_loading.xml index 20275d46..e6c08758 100644 --- a/app/src/main/res/layout/shader_loading.xml +++ b/app/src/main/res/layout/pipeline_loading.xml @@ -6,7 +6,7 @@ android:layout_height="match_parent"> @@ -80,7 +80,7 @@ Input Text - Compiling cached shaders... + Compiling cached pipelines... Expand Undo