diff --git a/Assets/Tests/Editor/GameViewBridgeTests.cs b/Assets/Tests/Editor/GameViewBridgeTests.cs new file mode 100644 index 0000000000..ae83df5a0b --- /dev/null +++ b/Assets/Tests/Editor/GameViewBridgeTests.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using NUnit.Framework; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.InternalAPIBridge; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies PlayModeView RenderTexture field resolution used by GameViewBridge. + /// + public class GameViewBridgeTests + { + private class FakePlayModeView + { +#pragma warning disable CS0414 // Field is assigned for reflection-based tests only. + private object m_TargetTexture = "texture"; +#pragma warning restore CS0414 + } + + private class FakeSimulatorWindow : FakePlayModeView + { + } + + [Test] + public void ResolveTargetTextureField_WhenCalledOnDeclaringType_FindsPrivateBaseField() + { + // Verifies GetField on the PlayModeView declaring type finds m_TargetTexture. + FieldInfo field = GameViewBridge.ResolveTargetTextureField(typeof(FakePlayModeView)); + + Assert.That(field, Is.Not.Null); + Assert.That(field.Name, Is.EqualTo("m_TargetTexture")); + Assert.That(field.DeclaringType, Is.EqualTo(typeof(FakePlayModeView))); + } + + [Test] + public void ResolveTargetTextureField_WhenDerivedTypeGetFieldMissesPrivateBaseField_DeclaringTypeStillResolves() + { + // Verifies the reflection pitfall: derived-type GetField cannot see private base fields. + FieldInfo fromDerived = typeof(FakeSimulatorWindow).GetField( + "m_TargetTexture", + BindingFlags.Instance | BindingFlags.NonPublic); + FieldInfo fromDeclaring = GameViewBridge.ResolveTargetTextureField(typeof(FakePlayModeView)); + + Assert.That(fromDerived, Is.Null); + Assert.That(fromDeclaring, Is.Not.Null); + + FakeSimulatorWindow instance = new(); + object value = fromDeclaring.GetValue(instance); + Assert.That(value, Is.EqualTo("texture")); + } + + [Test] + public void GetRenderTexture_WhenMembersResolved_DoesNotThrow() + { + // Verifies PlayModeView type/method/field resolution completes without throwing. + Assert.DoesNotThrow(() => GameViewBridge.GetRenderTexture()); + } + } +} diff --git a/Assets/Tests/Editor/GameViewBridgeTests.cs.meta b/Assets/Tests/Editor/GameViewBridgeTests.cs.meta new file mode 100644 index 0000000000..3cee14f1dc --- /dev/null +++ b/Assets/Tests/Editor/GameViewBridgeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72e1206d01daa484da9b23ccb96473ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef b/Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef index 22786365d0..c95c782558 100644 --- a/Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef +++ b/Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.Editor", "references": [ "GUID:214998e563c124e8a88199b2dd1f522d", + "GUID:5079a8d3a72924a81aa1cbc25f65ed1b", "GUID:5c4588558a3624eacbce0f50007cf1eb", "GUID:18e6dd30a99d14f32a045f79a2956f46", "GUID:2fca906c8834d45aea12a5cc143c032d", diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs index 09e10a2d8b..e62cf0bebc 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs @@ -162,7 +162,8 @@ public static string[] GetOpenWindowNames() return names.ToArray(); } - // Captures game rendering by reading GameView's composited RenderTexture (PlayMode only). + // Captures game rendering by reading the Play Mode view RenderTexture (PlayMode only). + // Works for both GameView and Device Simulator via PlayModeView.m_TargetTexture. // Contains all cameras + Screen Space Overlay Canvas, without tab bar or borders. internal static async Task<(Texture2D? texture, GameRenderingImageInfo renderingImageInfo, bool timedOut)> CaptureGameRenderingAsync( float resolutionScale, @@ -189,13 +190,13 @@ public static string[] GetOpenWindowNames() RenderTexture rt = GameViewBridge.GetRenderTexture(); if (rt == null) { - Debug.LogWarning("[EditorWindowCaptureUtility] GameView RenderTexture is not available"); + Debug.LogWarning("[EditorWindowCaptureUtility] Play Mode view RenderTexture is not available"); GameRenderingImageInfo unavailableInfo = renderingImageInfo ?? CreateUnavailableGameRenderingImageInfo(Handles.GetMainGameViewSize()); return (null, unavailableInfo, false); } - // GameView RenderTexture can be shorter than the full input area, so raw image Y needs this offset. + // Play Mode view RenderTexture can be shorter than the full input area, so raw image Y needs this offset. GameRenderingImageInfo captureInfo = renderingImageInfo ?? CreateGameRenderingImageInfo(Handles.GetMainGameViewSize(), rt.width, rt.height); diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs index 3317a49e65..42b5a2ba5c 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs @@ -168,7 +168,7 @@ private async Task CaptureRenderingAsync( if (captureTimedOut) { return CreateTimedOutResult( - "GameView rendering capture", + "Play Mode view rendering capture", correlationId, new List()); } @@ -190,7 +190,7 @@ private async Task CaptureRenderingAsync( { VibeLogger.LogError( "screenshot_rendering_unavailable", - "GameView RenderTexture is not available. Open the Game view and wait for a frame before retrying.", + "Play Mode view RenderTexture is not available. Open the Game view or Device Simulator and wait for a frame before retrying.", correlationId: correlationId ); return new ScreenshotResponse(); diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs index a78de55a98..ed182dfbfc 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs @@ -9,7 +9,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { // Creates a temporary Screen Space Overlay Canvas that draws bounding boxes and labels - // over interactive UI elements. The overlay is captured by GameView's m_RenderTexture + // over interactive UI elements. The overlay is captured by PlayModeView.m_TargetTexture // (OnGUI-based overlays are NOT included in the RT). /// /// Provides UI Element Annotator behavior for Unity CLI Loop. diff --git a/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs b/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs new file mode 100644 index 0000000000..dfa2c7f965 --- /dev/null +++ b/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")] diff --git a/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs.meta b/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs.meta new file mode 100644 index 0000000000..4ccb36acb7 --- /dev/null +++ b/Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df8bc76e5b3594fb98fc546217349bb1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/InternalAPIBridge/GameViewBridge.cs b/Packages/src/Editor/InternalAPIBridge/GameViewBridge.cs index 518fb86b4a..71bc674e2f 100644 --- a/Packages/src/Editor/InternalAPIBridge/GameViewBridge.cs +++ b/Packages/src/Editor/InternalAPIBridge/GameViewBridge.cs @@ -6,56 +6,59 @@ namespace io.github.hatayama.UnityCliLoop.InternalAPIBridge { /// - /// Bridge class for accessing Unity GameView internal APIs via reflection. - /// GameView is an internal class; this bridge discovers members dynamically. + /// Bridge for the active Play Mode view RenderTexture via reflection. + /// Uses PlayModeView so both GameView and Device Simulator windows work. /// public static class GameViewBridge { - private static Type _gameViewType; - private static FieldInfo _renderTextureField; + private const string PlayModeViewTypeName = "UnityEditor.PlayModeView"; + private const string GetMainPlayModeViewMethodName = "GetMainPlayModeView"; + private const string TargetTextureFieldName = "m_TargetTexture"; + + private static Type _playModeViewType; + private static MethodInfo _getMainPlayModeViewMethod; + private static FieldInfo _targetTextureField; private static bool _memberSearchDone; /// - /// Get the GameView's composited RenderTexture containing all cameras + Screen Space Overlay Canvas. + /// Get the active Play Mode view's composited RenderTexture + /// (cameras + Screen Space Overlay Canvas). /// - /// The RenderTexture, or null if GameView not found or field not accessible + /// The RenderTexture, or null if the view or field is unavailable public static RenderTexture GetRenderTexture() { EnsureMembersResolved(); - EditorWindow gameView = FindMainGameView(); - if (gameView == null || _renderTextureField == null) + EditorWindow playModeView = FindMainPlayModeView(); + if (playModeView == null || _targetTextureField == null) { return null; } - return _renderTextureField.GetValue(gameView) as RenderTexture; + return _targetTextureField.GetValue(playModeView) as RenderTexture; } - private static EditorWindow FindMainGameView() + /// + /// Resolve m_TargetTexture on the PlayModeView declaring type. + /// Must not use a derived Type: GetField does not return private fields declared on base types. + /// + internal static FieldInfo ResolveTargetTextureField(Type playModeViewType) { - if (_gameViewType == null) - { - return null; - } + Debug.Assert(playModeViewType != null, "playModeViewType must not be null"); - UnityEngine.Object[] gameViews = Resources.FindObjectsOfTypeAll(_gameViewType); - if (gameViews.Length == 0) - { - return null; - } + return playModeViewType.GetField( + TargetTextureFieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + } - // Prefer focused window to match editor's active view context - foreach (UnityEngine.Object gv in gameViews) + private static EditorWindow FindMainPlayModeView() + { + if (_getMainPlayModeViewMethod == null) { - EditorWindow window = gv as EditorWindow; - if (window != null && window.hasFocus) - { - return window; - } + return null; } - return gameViews[0] as EditorWindow; + return _getMainPlayModeViewMethod.Invoke(null, null) as EditorWindow; } private static void EnsureMembersResolved() @@ -66,18 +69,27 @@ private static void EnsureMembersResolved() } _memberSearchDone = true; - _gameViewType = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); - if (_gameViewType == null) + _playModeViewType = typeof(Editor).Assembly.GetType(PlayModeViewTypeName); + if (_playModeViewType == null) { - Debug.LogWarning("[GameViewBridge] GameView type not found"); + Debug.LogWarning("[GameViewBridge] PlayModeView type not found"); return; } - _renderTextureField = _gameViewType.GetField("m_RenderTexture", - BindingFlags.Instance | BindingFlags.NonPublic); - if (_renderTextureField == null) + _getMainPlayModeViewMethod = _playModeViewType.GetMethod( + GetMainPlayModeViewMethodName, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + if (_getMainPlayModeViewMethod == null) + { + Debug.LogWarning("[GameViewBridge] GetMainPlayModeView method not found"); + return; + } + + // why: private base fields are invisible to GetField on derived types (GameView / SimulatorWindow) + _targetTextureField = ResolveTargetTextureField(_playModeViewType); + if (_targetTextureField == null) { - Debug.LogWarning("[GameViewBridge] m_RenderTexture field not found"); + Debug.LogWarning("[GameViewBridge] m_TargetTexture field not found on PlayModeView"); } } }