Search Unity

Resolved How to stop an async function after completion

Discussion in 'Scripting' started by JSmithIR, May 20, 2023.

  1. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    I am new to asynchronous functions and have a simple script that saves a material into a png file using file.writeallbytesasync. I call the async function in update() when the user presses the "r" key. Everything works fine except that after the user presses "r" a single time, the async task begins and does not stop: ie, it keeps saving the current frame into the same file path. rewriting the previous png file.

    Then I did a test to save the material into a png using the framecount in the file name, and low and behold, when the user presses "r" the app starting writing hundreds of png files to disk (one for each frame being captured).

    How do I make the app asynchronously write to a png file ONE TIME when the user presses a button?

    Thanks for your help
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,860
    No one can help without showing your code.
     
  3. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    Thanks - code below is for the first describe situation - png name doesn't change and thus current frame continually gets saved over the last frame. I just want to save one frame and then have the task terminate precisely when the saving is completed.

    Code (CSharp):
    1. using Unity.Collections;
    2. using UnityEngine;
    3. using UnityEngine.Rendering;
    4. using OperationCanceledException = System.OperationCanceledException;
    5.  
    6. using System.IO;
    7. using UnityEngine.Networking;
    8. using System.Linq;
    9. using UnityEditor;
    10. using System.Threading;
    11. using System.Threading.Tasks;
    12. using System.Diagnostics;
    13.  
    14.  
    15. public class AsyncSequence : MonoBehaviour
    16. {
    17.     [SerializeField] float _interval = 0;
    18.     [SerializeField]
    19.     Material ImageMaterial;
    20.  
    21.  
    22.     [SerializeField]
    23.     public float time = 0.0f;
    24.     string _outputFileName;
    25.  
    26.  
    27.  
    28.     // Start is called before the first frame update
    29.     void Start()
    30.     {
    31.    
    32.     }
    33.  
    34.     // Update is called once per frame
    35.     void Update()
    36.     {
    37.  
    38.         _outputFileName = Application.persistentDataPath + "/Maps/" + "test" + time.ToString() + ".png";
    39.  
    40.         //if (counter % 2.0f > 0.0f)
    41.         if (Input.GetKey("r"))
    42.  
    43.         {
    44.             Capture();
    45.  
    46.         }
    47.  
    48.  
    49.  
    50.         async Task Capture()
    51.         {
    52.  
    53.             var (w, h) = (Screen.width, Screen.height);
    54.             var (scale, offs) = (new Vector2(1, -1), new Vector2(0, 1));
    55.  
    56.  
    57.             var RT = new RenderTexture(w, h, 0);
    58.             var rtFormat = RT.graphicsFormat;
    59.  
    60.             var buffer = new NativeArray<byte>(w * h * 4, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
    61.  
    62.             try
    63.             {
    64.                 for (var cancel = destroyCancellationToken; ;)
    65.                 //for (var cancel = token; ;)
    66.                 {
    67.                     cancel.ThrowIfCancellationRequested();
    68.                     // Interval (cancellable)
    69.  
    70.                     await Awaitable.WaitForSecondsAsync(_interval, cancel);
    71.                     await Awaitable.EndOfFrameAsync(cancel);
    72.  
    73.                     // Screen capture
    74.                     ScreenCapture.CaptureScreenshotIntoRenderTexture(RT);
    75.                     Graphics.Blit(null, RT);
    76.  
    77.                     // Asynchronous GPU readback
    78.                     var req = await AsyncGPUReadback.RequestIntoNativeArrayAsync(ref buffer, RT, 0);
    79.  
    80.                     if (req.hasError)
    81.                     {
    82.                         UnityEngine.Debug.Log("GPU readback error detected.");
    83.                         continue;
    84.                     }
    85.  
    86.                     // Check if it's cancelled before going background.
    87.                     cancel.ThrowIfCancellationRequested();
    88.  
    89.                     // PNG encoding in background: The ImageConversion methods are
    90.                     // thread safe (documented on the scripting reference), so we
    91.                     // can run it in a background thread.
    92.                     await Awaitable.BackgroundThreadAsync();
    93.                     using var encoded = ImageConversion.EncodeNativeArrayToPNG(buffer, rtFormat, (uint)w, (uint)h);
    94.  
    95.                     await Awaitable.MainThreadAsync();
    96.  
    97.                     // Asynchronous file output
    98.                     await File.WriteAllBytesAsync(_outputFileName, encoded.ToArray(), cancel);
    99.  
    100.                 }
    101.             }
    102.             catch (OperationCanceledException)
    103.             {
    104.             }
    105.             finally
    106.             {
    107.  
    108.                 if (RT != null) Destroy(RT);
    109.            
    110.                 if (buffer.IsCreated) buffer.Dispose();
    111.             }
    112.  
    113.         }
    114.     }
    115. }
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,860
    You probably want to use GetKeyDown, rather than GetKey, as a new async function will start for every frame you have the button held down.
     
    Last edited: May 20, 2023
    tsukimi likes this.
  5. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    It continues saving frames five minutes after the key is presssed tho. Thats not the problem
     
    minnine likes this.
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,860
    Have you tried my suggestion?

    By the by it should be an
    async void
    as you aren't waiting for the completion of its execution.
     
  7. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    Initially, no, I did not need to try your suggestion to know it was wrong. But I tried it just now anyway to prove to myself it is wrong. Lo and behold, it does not fix the problem
     
  8. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,065
    Spiney's suggestion might not address your stated problem but it does address an important issue in your code that you better fix to make debugging easier.

    What are you trying to with this for loop?
    Code (CSharp):
    1. for (var cancel = destroyCancellationToken; ;)
    2. {
    3.     // …
    4. }
    That's almost the same as writing:
    Code (CSharp):
    1. var cancel = destroyCancellationToken;
    2. while (true)
    3. {
    4.     // …
    5. }
    i.e. you're creating an infinite loop that keeps capturing the screen and writing it out as a png.

    Are you trying to retry the capture in case it fails? Using a
    while (true)
    loop would be more obvious in that case and you need to break out of the loop in case the capture succeeded. I'd also limit the number of attempts so that it doesn't continue indefinitely in case of a permanent error.

    But just removing the for loop seems to make the most sense.
     
    Bunny83 likes this.
  9. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    What you're missing is creating a
    CancellationToken
    and passing it down to the Capture method.
    CancellationToken
    s can be created using the
    CancellationTokenSource
    class.

    Code (CSharp):
    1. CancellationTokenSource cts;
    2.  
    3. private void Update
    4. {
    5.     if (KeyDown)
    6.     {
    7.         // Cancel old tasks...
    8.         cts?.Cancel();
    9.  
    10.         cts = new CancellationTokenSource();
    11.         Capture(cts.Token);
    12.     }
    13. }
    14.  
    15. private async Task Capture(CancellationToken cancellationToken)
    16. {
    17.     while (!cancellationToken.IsCancellationRequested)
    18.     {
    19.         await Awaitable.WaitForSecondsAsync(_interval, cancellationToken);
    20.         await Awaitable.EndOfFrameAsync(cancellationToken); // Screen capture
    21.  
    22.         // Another check here could be helpful
    23.         if (cancellationToken.IsCancellationRequested)
    24.             return;
    25.  
    26.         ScreenCapture.CaptureScreenshotIntoRenderTexture(RT);
    27.         Graphics.Blit(null, RT);
    28.         // Continue capture code...
    29.     }
    30. }
     
    Last edited: May 20, 2023
    JSmithIR likes this.
  10. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    Hey thanks for catching this - you are correct. Solved the problem! The cts cancellation token source was a remnant from something I was trying before, but obviously the destroy cancellation token was never being triggered so it kept looping
     
  11. JSmithIR

    JSmithIR

    Joined:
    Apr 13, 2023
    Posts:
    111
    Thanks that is a great solution for something I was trying earlier. Helps me better understand how to use cancellation tokens. Appreciate it man
     
    Nad_B likes this.