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
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Author: Miguel A. Lopez
* Company: Rank Up Games LLC
* Project: Unity Cursor Toolkit
* Description: Captures the current application camera view for editor diagnostics.
*/

#if UNITY_EDITOR
using System;
using System.IO;

using UnityEngine;

namespace UnityCursorToolkit
{
/// <summary>
/// Captures the current application view from the main camera to one stable temp file.
/// </summary>
internal static class ApplicationScreenshotCapture
{
private const string ScreenshotFileName = "unity-cursor-toolkit-application.png";

internal static string ScreenshotPath => Path.Combine(Application.temporaryCachePath, ScreenshotFileName);

internal static bool TryCapture(out string path, out string error)
{
path = ScreenshotPath;
error = null;

Camera camera = Camera.main;
if (camera == null)
{
error = "No main camera found";
return false;
}

int width = Math.Max(1, Screen.width);
int height = Math.Max(1, Screen.height);
RenderTexture previousTarget = camera.targetTexture;
RenderTexture previousActive = RenderTexture.active;
RenderTexture renderTexture = null;
Texture2D texture = null;

try
{
renderTexture = RenderTexture.GetTemporary(width, height, 24);
camera.targetTexture = renderTexture;
camera.Render();

RenderTexture.active = renderTexture;
texture = new Texture2D(width, height, TextureFormat.RGB24, false);
texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
texture.Apply();
File.WriteAllBytes(path, texture.EncodeToPNG());
return true;
}
catch (Exception exception)
{
error = exception.Message;
return false;
}
finally
{
camera.targetTexture = previousTarget;
RenderTexture.active = previousActive;
if (texture != null)
{
UnityEngine.Object.DestroyImmediate(texture);
}
if (renderTexture != null)
{
RenderTexture.ReleaseTemporary(renderTexture);
}
}
}
}
}

#endif // UNITY_EDITOR

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

Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ static MainToolbarButton CreateMainToolbarCopyLogsButton()
{
Texture2D _icon = EditorGUIUtility.IconContent("Clipboard").image as Texture2D;
return new MainToolbarButton(
new MainToolbarContent(_icon, "Copy profiler session and console transcript context"),
new MainToolbarContent(_icon, "Copy profiler session, console transcript, and application screenshot path"),
() => CopyConsoleLogs());
}
#endif
Expand All @@ -49,7 +49,19 @@ static MainToolbarButton CreateMainToolbarCopyLogsButton()
[MenuItem("Tools/Unity Cursor Toolkit/Copy Console Logs %#l")]
internal static void CopyConsoleLogs()
{
GUIUtility.systemCopyBuffer = ProfilerSessionRecorder.BuildClipboardSnapshot(ProfilerSnapshotSettings.Current.IncludeRawFrameArrays);
string clipboardContent = ProfilerSessionRecorder.BuildClipboardSnapshot(ProfilerSnapshotSettings.Current.IncludeRawFrameArrays);
string screenshotPath;
string screenshotError;
if (ApplicationScreenshotCapture.TryCapture(out screenshotPath, out screenshotError))
{
clipboardContent += "\n\nApplication screenshot: " + screenshotPath;
}
else
{
Debug.LogWarning("(ConsoleLogCopyTool - CopyConsoleLogs) Application screenshot was not captured: " + screenshotError);
}

GUIUtility.systemCopyBuffer = clipboardContent;
Debug.Log("(ConsoleLogCopyTool - CopyConsoleLogs) Copied current profiler session path, console transcript path, and error summary to clipboard");
}
}
Expand Down
21 changes: 21 additions & 0 deletions unity-cursor-toolkit/test/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,8 @@ function testUnityProfilerSafetySource() {
const hotReloadSource = fs.readFileSync(path.join(editorRoot, 'HotReloadHandler.cs'), 'utf8');
const validationSource = fs.readFileSync(path.join(editorRoot, 'MCP', 'EditorValidationTool.cs'), 'utf8');
const editorControlSource = fs.readFileSync(path.join(editorRoot, 'MCP', 'EditorControlTools.cs'), 'utf8');
const copySource = fs.readFileSync(path.join(editorRoot, 'ConsoleLogCopyTool.cs'), 'utf8');
const screenshotSource = fs.readFileSync(path.join(editorRoot, 'ApplicationScreenshotCapture.cs'), 'utf8');

test('background profiler is Play-Mode-only and not reconfigured from Tick', () => {
const tickStart = profilerSource.indexOf('private static void Tick()');
Expand All @@ -1243,6 +1245,25 @@ function testUnityProfilerSafetySource() {
assert.ok(profilerSource.includes('private const long MaxTempSessionBytes = 64L * 1024L * 1024L;'));
});

test('console copy captures a stable application screenshot and preserves fallback copy', () => {
assert.ok(fs.existsSync(path.join(editorRoot, 'ApplicationScreenshotCapture.cs.meta')));
assert.ok(screenshotSource.includes('Application.temporaryCachePath'));
assert.ok(screenshotSource.includes('private const string ScreenshotFileName = "unity-cursor-toolkit-application.png"'));
assert.ok(screenshotSource.includes('Path.Combine(Application.temporaryCachePath, ScreenshotFileName)'));
assert.ok(screenshotSource.includes('Camera.main'));
assert.ok(screenshotSource.includes('File.WriteAllBytes(path, texture.EncodeToPNG())'));
assert.ok(screenshotSource.includes('RenderTexture.active = previousActive'));
assert.ok(screenshotSource.includes('camera.targetTexture = previousTarget'));
assert.ok(screenshotSource.includes('UnityEngine.Object.DestroyImmediate(texture)'));
assert.ok(screenshotSource.includes('RenderTexture.ReleaseTemporary(renderTexture)'));
assert.ok(screenshotSource.includes('finally'));
assert.ok(!screenshotSource.includes('DateTime.UtcNow.Ticks'));
assert.ok(copySource.includes('ApplicationScreenshotCapture.TryCapture'));
assert.ok(copySource.includes('Application screenshot: '));
assert.ok(copySource.includes('GUIUtility.systemCopyBuffer = clipboardContent'));
assert.ok(copySource.includes('Debug.LogWarning'));
});

test('refresh handling avoids duplicate compilation and bounds queued message bytes', () => {
assert.ok(hotReloadSource.includes('private const int MAX_QUEUED_MESSAGE_CHARACTERS = 4 * 1024 * 1024;'));
assert.ok(hotReloadSource.includes('EnqueueMessage(line);'));
Expand Down