Skip to content

Instantly share code, notes, and snippets.

@longod
Last active September 18, 2023 20:24
Show Gist options
  • Save longod/a94057c7171addd28972d31747b9ccd5 to your computer and use it in GitHub Desktop.
Save longod/a94057c7171addd28972d31747b9ccd5 to your computer and use it in GitHub Desktop.
Asynchronous Capture Screen on Unity 2019.3
// (c) longod, MIT License
using System;
using System.Collections;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
/// <summary>
/// Asynchronous Capture Screen
/// require Unity 2019.3 or later
/// </summary>
public class AsyncScreenCapture : IDisposable {
public IEnumerator CaptureAsync(string uniquePath) {
yield return new WaitForEndOfFrame();
Profiler.BeginSample( "AsyncScreenCapture.CaptureAsync" );
int width = Screen.width;
int height = Screen.height;
GraphicsFormat graphicsFormat = GraphicsFormat.R8G8B8A8_SRGB;
var index = FindUsableTexture( width, height, graphicsFormat );
if (index < 0) {
Debug.LogError( "not enough buffer" );
} else {
ScreenCapture.CaptureScreenshotIntoRenderTexture( buffer[ index ].renderTexture );
buffer[ index ].request = AsyncGPUReadback.Request( buffer[ index ].renderTexture, 0, graphicsFormat, (request) => { ReadbackCompleted( request, uniquePath, buffer[ index ].renderTexture ); } );
}
Profiler.EndSample();
}
public void Dispose() {
for (int i = 0; i < buffer.Length; ++i) {
if (!buffer[ i ].request.done) {
buffer[ i ].request.WaitForCompletion(); // sync
if (buffer[ i ].renderTexture != null) {
UnityEngine.Object.Destroy( buffer[ i ].renderTexture );
buffer[ i ].renderTexture = null;
}
}
}
buffer = null;
}
private void ReadbackCompleted(AsyncGPUReadbackRequest request, string path, RenderTexture renderTexture) {
if (flipSampler == null) {
flipSampler = CustomSampler.Create( "AsyncScreenCapture.FlipY" );
}
if (encodeSampler == null) {
encodeSampler = CustomSampler.Create( "AsyncScreenCapture.Encode" );
}
if (writeSampler == null) {
writeSampler = CustomSampler.Create( "AsyncScreenCapture.Write" );
}
Profiler.BeginSample( "AsyncScreenCapture.ReadbackCompleted" );
uint width = (uint)renderTexture.width;
uint height = (uint)renderTexture.height;
var graphicsFormat = renderTexture.graphicsFormat;
var managed = request.GetData<byte>().ToArray();
// 専用の単一スレッドで実行すると同時に同じパスに書き込む可能性は解消できるが、詰まりやすくなる
Task.Run( () => {
Profiler.BeginThreadProfiling( "Task", $"Thread {Thread.CurrentThread.ManagedThreadId}" );
flipSampler.Begin();
var image = new byte[ managed.Length ];
int pitch = 4 * (int)width; // R8G8B8A8
for (int y = 0; y < height; ++y) {
Buffer.BlockCopy( managed, pitch * y, image, ((int)height - 1 - y) * pitch, pitch );
}
flipSampler.End();
encodeSampler.Begin();
byte[] bin = ImageConversion.EncodeArrayToPNG( image, graphicsFormat, width, height );
encodeSampler.End();
writeSampler.Begin();
File.WriteAllBytes( path, bin );
writeSampler.End();
Profiler.EndThreadProfiling();
} );
Profiler.EndSample();
}
private int FindUsableTexture(int width, int height, GraphicsFormat graphicsFormat) {
int index = -1;
for (int i = 0; i < buffer.Length; ++i) {
if (buffer[ i ].request.done) {
if (index < 0) {
index = i;
}
if (buffer[ i ].renderTexture != null &&
buffer[ i ].renderTexture.width == width &&
buffer[ i ].renderTexture.height == height &&
buffer[ i ].renderTexture.graphicsFormat == graphicsFormat) {
// found reusable texture
return i;
}
}
}
// recreate
if (index >= 0) {
if (buffer[ index ].renderTexture != null) {
UnityEngine.Object.Destroy( buffer[ index ].renderTexture );
}
buffer[ index ].renderTexture = new RenderTexture( width, height, 0, graphicsFormat, 0 );
}
return index;
}
struct RequestingBuffer {
internal AsyncGPUReadbackRequest request;
internal RenderTexture renderTexture;
}
RequestingBuffer[] buffer = new RequestingBuffer[ maxBufferCount ];
// RenderTextureが要求される寿命は、CaptureScreenshotIntoRenderTextureから、
// リードバック完了(コールバック呼び出し開始時まで)、それ以降は破棄されても問題ない。
// ハードウェアと解像度によるが、大体リードバックは2,3フレームで完了するので、
// 高解像度で毎フレーム1枚撮ったとしてもこれくらいあれば十分だろうというバッファ数。
static readonly int maxBufferCount = 8;
static CustomSampler flipSampler = null;
static CustomSampler encodeSampler = null;
static CustomSampler writeSampler = null;
}
@winco-ricky-rain
Copy link

seems no use on android

@longod
Copy link
Author

longod commented Aug 17, 2021

Hi, @winco-ricky-rain.
I have never tested it on anything other than a PC because I don't have android development environment.
At least, I think the output path should be Application.persistentDataPath.
Then the device's GPU and Graphics APIs must support AsyncGPUReadback, check if SystemInfo.supportsAsyncGPUReadback returns true.

@tremayne-stewart
Copy link

@winco-ricky-rain Can you be more specific? I know that the use of ScreenCapture.CaptureScreenshotIntoRenderTexture is going to give you a black render texture, but did you notice any performance issues when you tested ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment