diff --git a/debugger/texture-debugger/TextureUtils.cs b/debugger/texture-debugger/TextureUtils.cs new file mode 100644 index 0000000000..5fe45fa7e0 --- /dev/null +++ b/debugger/texture-debugger/TextureUtils.cs @@ -0,0 +1,123 @@ +using System.Drawing; +using System.Linq; +using UnityEngine; +using Graphics = UnityEngine.Graphics; +using Object = UnityEngine.Object; + +namespace JetBrains.Debugger.Worker.Plugins.Unity.Presentation.Texture +{ + public class TexturePixelsInfo + { + public int Width; + public int Height; + public int[] Pixels; + public int OriginalWidth; + public int OriginalHeight; + public string GraphicsTextureFormat; + public string TextureFormat; + + + public TexturePixelsInfo(Size size, Color32[] pixels, Texture2D texture2D) + { + Pixels = pixels.Select(c => c.ToHex()).ToArray(); + Width = size.Width; + Height = size.Height; + TextureFormat = texture2D.format.ToString(); + GraphicsTextureFormat = texture2D.graphicsFormat.ToString(); + OriginalWidth = texture2D.width; + OriginalHeight = texture2D.height; + } + } + + public static class UnityTextureAdapter + { + public static int ToHex(this Color32 c) + { + return (c.r << 16) | (c.g << 8) | (c.b); + } + + public static string GetPixelsInString(Texture2D texture2D) + { + return GetPixelsInString(texture2D, new Size(texture2D.width, texture2D.height)); + } + + public static string GetPixelsInString(Texture2D texture2D, Size size) + { + size = GetTextureConvertedSize(texture2D, size); + var color32 = GetPixels(texture2D, size); + return JsonUtility.ToJson(color32, true); + } + + private static TexturePixelsInfo GetPixels(Texture2D texture2d, Size size) + { + var targetTexture = CreateTargetTexture(size); + CopyTexture(texture2d, targetTexture); + var pixels = targetTexture.GetPixels32(); + var texturePixelsInfo = new TexturePixelsInfo(new Size(targetTexture.width, targetTexture.height) + , pixels + , texture2d); + Object.DestroyImmediate(targetTexture); + return texturePixelsInfo; + } + + private static byte[] GetRawBytes(Texture2D texture2d, Size size) + { + var targetTexture = CreateTargetTexture(size); + CopyTexture(texture2d, targetTexture); + var rawTextureData = targetTexture.GetRawTextureData(); + Object.DestroyImmediate(targetTexture); + return rawTextureData; + } + + private static void CopyTexture(UnityEngine.Texture texture2d, Texture2D targetTexture) + { + var renderTexture = RenderTexture.GetTemporary( + targetTexture.width, + targetTexture.height, + 0, + RenderTextureFormat.ARGB32 + ); + + // Blit the pixels on texture to the RenderTexture + Graphics.Blit(texture2d, renderTexture); + + // Backup the currently set RenderTexture + var previous = RenderTexture.active; + + // Set the current RenderTexture to the temporary one we created + RenderTexture.active = renderTexture; + + // Create a new readable Texture2D to copy the pixels to it + + // Copy the pixels from the RenderTexture to the new Texture + targetTexture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); + targetTexture.Apply(); + + // Reset the active RenderTexture + RenderTexture.active = previous; + + // Release the temporary RenderTexture + RenderTexture.ReleaseTemporary(renderTexture); + } + + private static Texture2D CreateTargetTexture(Size size) + { + var texture2D = new Texture2D(size.Width, size.Height, TextureFormat.RGBA32, false); + return texture2D; + } + + private static Size GetTextureConvertedSize(UnityEngine.Texture texture2d, Size size) + { + var texture2dWidth = texture2d.width; + var texture2dHeight = texture2d.height; + + var divider = 1; + while (texture2dWidth / divider > size.Width && texture2dHeight / divider > size.Height) + divider *= 2; + + var targetTextureWidth = texture2dWidth / divider; + var targetTextureHeight = texture2dHeight / divider; + return new Size(targetTextureWidth, targetTextureHeight); + } + } +} \ No newline at end of file diff --git a/debugger/texture-debugger/texture-debugger.csproj b/debugger/texture-debugger/texture-debugger.csproj new file mode 100644 index 0000000000..f3f96122b8 --- /dev/null +++ b/debugger/texture-debugger/texture-debugger.csproj @@ -0,0 +1,32 @@ + + + + True + False + + + + + + net472 + JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture + JetBrains.Debugger.Worker.Plugins.Unity.Presentation.Texture + 9 + ..\..\sign.snk + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/resharper/resharper-unity.sln b/resharper/resharper-unity.sln index d3bba570c9..5eb0d77a6e 100644 --- a/resharper/resharper-unity.sln +++ b/resharper/resharper-unity.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EditorPlugin.SinceUnity.201 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EditorPlugin.SinceUnity.2019.2", "..\unity\EditorPlugin\EditorPlugin.SinceUnity.2019.2.csproj", "{117CAF5D-7FC0-4626-98E0-E81572964E56}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "texture-debugger", "..\debugger\texture-debugger\texture-debugger.csproj", "{DF886D30-1FE9-4296-9202-BCE567BFF2E5}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\debugger\usbmuxd\usbmuxd.projitems*{4e49cd68-6ba2-4765-80ac-d82537a0996b}*SharedItemsImports = 5 @@ -144,6 +146,10 @@ Global {117CAF5D-7FC0-4626-98E0-E81572964E56}.Debug|Any CPU.Build.0 = Debug|Any CPU {117CAF5D-7FC0-4626-98E0-E81572964E56}.Release|Any CPU.ActiveCfg = Release|Any CPU {117CAF5D-7FC0-4626-98E0-E81572964E56}.Release|Any CPU.Build.0 = Release|Any CPU + {DF886D30-1FE9-4296-9202-BCE567BFF2E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF886D30-1FE9-4296-9202-BCE567BFF2E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF886D30-1FE9-4296-9202-BCE567BFF2E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF886D30-1FE9-4296-9202-BCE567BFF2E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -170,6 +176,7 @@ Global {CA4516C7-2795-4F5D-B13C-EC5E46F19C68} = {27FAB5DF-2272-44E7-9BCE-41508F24C07B} {27D56D2A-B11F-4BAA-B26B-1A73A055B398} = {27FAB5DF-2272-44E7-9BCE-41508F24C07B} {117CAF5D-7FC0-4626-98E0-E81572964E56} = {27FAB5DF-2272-44E7-9BCE-41508F24C07B} + {DF886D30-1FE9-4296-9202-BCE567BFF2E5} = {D61F53F1-F209-4313-9FF2-B1DBD286BE11} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F132919A-F861-4C4F-923A-C956B82D9EE6} diff --git a/rider/build.gradle.kts b/rider/build.gradle.kts index b53f578bc2..4a40a19e99 100644 --- a/rider/build.gradle.kts +++ b/rider/build.gradle.kts @@ -93,6 +93,11 @@ val debuggerDllFiles = files( "../resharper/build/debugger/bin/$buildConfiguration/net472/JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.pdb" ) +val textureDebuggerDllFiles = files( + "../resharper/build/texture-debugger/bin/$buildConfiguration/net472/JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture.dll", + "../resharper/build/texture-debugger/bin/$buildConfiguration/net472/JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture.pdb" +) + val listIosUsbDevicesFiles = files( "../resharper/build/ios-list-usb-devices/bin/$buildConfiguration/net7.0/JetBrains.Rider.Unity.ListIosUsbDevices.dll", "../resharper/build/ios-list-usb-devices/bin/$buildConfiguration/net7.0/JetBrains.Rider.Unity.ListIosUsbDevices.pdb", @@ -900,6 +905,7 @@ See CHANGELOG.md in the JetBrains/resharper-unity GitHub repo for more details a doLast { dotnetDllFiles.forEach { if (!it.exists()) error("File $it does not exist") } debuggerDllFiles.forEach { if (!it.exists()) error("File $it does not exist") } + textureDebuggerDllFiles.forEach { if (!it.exists()) error("File $it does not exist") } listIosUsbDevicesFiles.forEach { if (!it.exists()) error("File $it does not exist") } unityEditorDllFiles.forEach { if (!it.exists()) error("File $it does not exist") } } @@ -908,6 +914,7 @@ See CHANGELOG.md in the JetBrains/resharper-unity GitHub repo for more details a dotnetDllFiles.forEach { from(it) { into("${pluginName}/dotnet") } } debuggerDllFiles.forEach { from(it) { into("${pluginName}/dotnetDebuggerWorker") } } + textureDebuggerDllFiles.forEach { from(it) { into("${pluginName}/DotFiles") } } listIosUsbDevicesFiles.forEach { from(it) { into("${pluginName}/DotFiles") } } unityEditorDllFiles.forEach { from(it) { into("${pluginName}/EditorPlugin") } } diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/debugger/visualizers/UnityDebuggerTexturePresenter.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/debugger/visualizers/UnityDebuggerTexturePresenter.kt new file mode 100644 index 0000000000..2da0ccb534 --- /dev/null +++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/debugger/visualizers/UnityDebuggerTexturePresenter.kt @@ -0,0 +1,228 @@ +package com.jetbrains.rider.plugins.unity.debugger.visualizers + +import com.google.gson.Gson +import com.intellij.openapi.project.Project +import com.intellij.openapi.rd.createNestedDisposable +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.ErrorLabel +import com.intellij.ui.components.JBLoadingPanel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.panels.VerticalLayout +import com.intellij.util.ui.ImageUtil +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import com.intellij.xdebugger.frame.XValue +import com.intellij.xdebugger.frame.XValueNode +import com.intellij.xdebugger.frame.XValuePlace +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl +import com.jetbrains.rd.framework.RdTaskResult +import com.jetbrains.rd.ide.model.ValuePropertiesModelBase +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.printlnError +import com.jetbrains.rd.util.reactive.adviseOnce +import com.jetbrains.rider.RiderEnvironment +import com.jetbrains.rider.debugger.DotNetValue +import com.jetbrains.rider.debugger.visualizers.RiderDebuggerPresenterTab +import com.jetbrains.rider.debugger.visualizers.RiderDebuggerValuePresenter +import com.jetbrains.rider.model.debuggerWorker.ComputeObjectPropertiesArg +import com.jetbrains.rider.model.debuggerWorker.FailedObjectProperties +import com.jetbrains.rider.model.debuggerWorker.ObjectPropertiesProxy +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.intellij.images.editor.impl.ImageEditorManagerImpl.createImageEditorUI +import java.awt.BorderLayout +import java.awt.GraphicsConfiguration +import java.awt.GraphicsEnvironment +import java.awt.Rectangle +import java.awt.geom.AffineTransform +import java.awt.image.BufferedImage +import java.awt.image.ColorModel +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.SwingConstants + +class UnityDebuggerTexturePresenter : RiderDebuggerValuePresenter { + + @Suppress("PropertyName") + data class TextureInfo(val Pixels: List, + val Width: Int, + val Height: Int, + val OriginalWidth: Int, + val OriginalHeight: Int, + val GraphicsTextureFormat: String, + val TextureFormat: String + ) + + override fun isApplicable(node: XValueNode, properties: ObjectPropertiesProxy, place: XValuePlace, session: XDebugSession): Boolean { + return properties.instanceType.definitionTypeFullName == "UnityEngine.Texture2D" + } + + override fun getPriority(): Int { + return 0 + } + + override fun shouldIgnorePropertiesReevaluation( + node: XValueNode, + properties: ObjectPropertiesProxy, + place: XValuePlace, + session: XDebugSession + ): Boolean { + return true + } + + + override fun createTabs( + node: XValueNode, + properties: ObjectPropertiesProxy, + place: XValuePlace, + session: XDebugSession, + lifetime: Lifetime + ): List { + val parentPanel = JBPanel>(VerticalLayout(5)) + val jbLoadingPanel = JBLoadingPanel(BorderLayout(), lifetime.createNestedDisposable()) + parentPanel.add(jbLoadingPanel) + jbLoadingPanel.startLoading() + parentPanel.revalidate() + parentPanel.repaint() + + val bundledFile = RiderEnvironment.getBundledFile( + "JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture.dll", + pluginClass = javaClass + ) + + val evaluationRequest = "System.Reflection.Assembly.LoadFile(@\"${bundledFile.absolutePath}\")" + val project = session.project + evaluate(project, evaluationRequest, lifetime, + successfullyEvaluated = { evaluateTextureAndShow(node, project, jbLoadingPanel, parentPanel, lifetime) }, + evaluationFailed = { + showErrorMessage( + jbLoadingPanel, + parentPanel, + "Can't load texture helpers dll:\n$it" + ) + }) + + val name = (node as XValueNodeImpl).rawValue!! + return listOf(RiderDebuggerPresenterTab(name, name, parentPanel, null)) + } + + private fun evaluateTextureAndShow(node: XValueNode, + project: Project, + jbLoadingPanel: JBLoadingPanel, + parentPanel: JBPanel>, + lifetime: Lifetime) { + val nodeName = (node as XValueNodeImpl).name!! + val evaluationRequest = "JetBrains.Debugger.Worker.Plugins.Unity.Presentation.Texture.UnityTextureAdapter.GetPixelsInString($nodeName as UnityEngine.Texture2D)" + evaluate(project, evaluationRequest, lifetime, + successfullyEvaluated = { showTexture(it, jbLoadingPanel, parentPanel) }, + evaluationFailed = { showErrorMessage(jbLoadingPanel, parentPanel, "Can't get texture debug information:\n$it") }) + } + + private fun showTexture(it: ValuePropertiesModelBase, + jbLoadingPanel: JBLoadingPanel, + parentPanel: JBPanel>) { + val textureInfo = parseTextureEvaluationResult(it) + + if (textureInfo == null) + showErrorMessage(jbLoadingPanel, parentPanel, "Can't parse texture info") + else { + val texturePanel = createPanelWithImage(textureInfo) + jbLoadingPanel.stopLoading() + parentPanel.remove(jbLoadingPanel) + + parentPanel.apply { + add(JLabel("Texture Size: ${textureInfo.OriginalWidth}x${textureInfo.OriginalHeight}")) + add(JLabel("Texture Format: ${textureInfo.TextureFormat}")) + add(JLabel("Texture Graphics Format: ${textureInfo.GraphicsTextureFormat}")) + } + parentPanel.add(texturePanel) + } + parentPanel.revalidate() + parentPanel.repaint() + } + + private fun parseTextureEvaluationResult(it: ValuePropertiesModelBase): TextureInfo? { + val json = it.value[0].value + return Gson().fromJson(json, TextureInfo::class.java) + } + + private fun showErrorMessage(jbLoadingPanel: JBLoadingPanel, + parentPanel: JBPanel>, + @NlsContexts.Label errorMessage: String) { + jbLoadingPanel.stopLoading() + parentPanel.remove(jbLoadingPanel) + parentPanel.add(ErrorLabel(errorMessage).apply { + verticalAlignment = SwingConstants.TOP + horizontalAlignment = SwingConstants.LEFT + }) + printlnError(errorMessage) + } + + + @Suppress("INACCESSIBLE_TYPE") + private fun createPanelWithImage(textureInfo: TextureInfo): JPanel { + + val defaultConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration + val dummyGraphicsConfiguration = object : GraphicsConfiguration() { + override fun getDevice() = defaultConfiguration.device + override fun getColorModel() = ColorModel.getRGBdefault() + override fun getColorModel(transparency: Int) = ColorModel.getRGBdefault() + override fun getDefaultTransform() = AffineTransform() + override fun getNormalizingTransform() = AffineTransform() + override fun getBounds() = Rectangle(0, 0, textureInfo.Width, textureInfo.Height) + } + + val bufferedImage = ImageUtil.createImage(dummyGraphicsConfiguration, + textureInfo.Width, textureInfo.Height, BufferedImage.TYPE_INT_RGB) + + for (y in 0 until textureInfo.Height) { + for (x in 0 until textureInfo.Width) { + val toInt = textureInfo.Pixels[y * textureInfo.Height + x] + bufferedImage.setRGB(x, textureInfo.Height - y - 1, toInt) + } + } + + return createImageEditorUI(bufferedImage) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun evaluate(project: Project, + evaluationRequest: String, + lifetime: Lifetime, + successfullyEvaluated: (value: ValuePropertiesModelBase) -> Unit, + evaluationFailed: (errorMessage: String) -> Unit) { + val evaluator = XDebuggerManager.getInstance(project).currentSession!!.currentStackFrame!!.evaluator!! + + evaluator.evaluate(evaluationRequest, + object : XDebuggerEvaluator.XEvaluationCallback { + override fun errorOccurred(errorMessage: String) = evaluationFailed(errorMessage) + override fun evaluated(loadDllresult: XValue) { + if (loadDllresult is DotNetValue) { + loadDllresult.objectProxy.computeObjectProperties.start( + lifetime, + ComputeObjectPropertiesArg(allowInvoke = true, + allowCrossThread = true, + ellipsizeStrings = false, + ellipsizedLength = null, + nameAliases = emptyList(), + extraInfo = null, + allowDisabledMethodsInvoke = null) + ).result.adviseOnce(lifetime) { + when (it) { + is RdTaskResult.Success -> { + val value = it.value + if (value is FailedObjectProperties) + evaluationFailed(value.value.joinToString("\n")) + else + successfullyEvaluated(value) + } + is RdTaskResult.Cancelled -> evaluationFailed("Cancelled $it") + is RdTaskResult.Fault -> evaluationFailed("Fault $it") + } + } + } + } + }, null) + } +} \ No newline at end of file diff --git a/rider/src/main/resources/META-INF/plugin.xml b/rider/src/main/resources/META-INF/plugin.xml index 4e6d915258..04fb223428 100644 --- a/rider/src/main/resources/META-INF/plugin.xml +++ b/rider/src/main/resources/META-INF/plugin.xml @@ -205,6 +205,7 @@ +