Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Assets/Tests/Editor/GameViewBridgeTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Verifies PlayModeView RenderTexture field resolution used by GameViewBridge.
/// </summary>
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());
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/GameViewBridgeTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.Editor",
"references": [
"GUID:214998e563c124e8a88199b2dd1f522d",
"GUID:5079a8d3a72924a81aa1cbc25f65ed1b",
"GUID:5c4588558a3624eacbce0f50007cf1eb",
"GUID:18e6dd30a99d14f32a045f79a2956f46",
"GUID:2fca906c8834d45aea12a5cc143c032d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ private async Task<ScreenshotResponse> CaptureRenderingAsync(
if (captureTimedOut)
{
return CreateTimedOutResult(
"GameView rendering capture",
"Play Mode view rendering capture",
correlationId,
new List<ScreenshotInfo>());
}
Expand All @@ -190,7 +190,7 @@ private async Task<ScreenshotResponse> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
/// <summary>
/// Provides UI Element Annotator behavior for Unity CLI Loop.
Expand Down
3 changes: 3 additions & 0 deletions Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")]
11 changes: 11 additions & 0 deletions Packages/src/Editor/InternalAPIBridge/AssemblyInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 46 additions & 34 deletions Packages/src/Editor/InternalAPIBridge/GameViewBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,59 @@
namespace io.github.hatayama.UnityCliLoop.InternalAPIBridge
{
/// <summary>
/// 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.
/// </summary>
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;

/// <summary>
/// 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).
/// </summary>
/// <returns>The RenderTexture, or null if GameView not found or field not accessible</returns>
/// <returns>The RenderTexture, or null if the view or field is unavailable</returns>
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()
/// <summary>
/// Resolve m_TargetTexture on the PlayModeView declaring type.
/// Must not use a derived Type: GetField does not return private fields declared on base types.
/// </summary>
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()
Expand All @@ -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");
}
}
}
Expand Down