Yes—you’re correct that you need a latent native function if you want to kick off your GIF capture in C++ and have Papyrus simply yield and resume once all frames are gathered, rather than blocking the game for the entire capture duration.

1. What “latent” means for native Papyrus functions

A latent function is one that does not return immediately to the script; instead the VM suspends (“yields”) that script thread, lets the game continue running, and only resumes your script once you explicitly return a value. Papyrus’s built‑in Utility.Wait(), Disable(), Enable(), etc., are examples of latent calls that pause the script across frames (Gist).

2. Registering a latent function in your SKSE plugin

If you’re using CommonLibSSE NG (the modern C++ wrapper for SKSE), it exposes an overload specifically for latent functions:

#include "RE/BSScript/IVirtualMachine.h" #include "RE/BSScript/Internal/VirtualMachine.h" // 1) The initial callback signature: BSScript::LatentStatus StartGIFCapture( RE::BSScript::Internal::VirtualMachine* vm, VMStackID stackID, RE::StaticFunctionTag*, // standard first param float durationSeconds, std::int32_t fps ) { // Kick off your async capture (on another thread or via timers). // Store 'vm' and 'stackID' in your capture manager, so you can // return to the right script stack once done. CaptureManager::Get().Begin( durationSeconds, fps, [vm, stackID](bool success) { // 3) Return to Papyrus when frames are all saved: vm->ReturnFromLatent(stackID, success); } ); // 2) Tell the VM “I’m latent—don’t resume yet”: return BSScript::LatentStatus::kLatent; } // Plugin registration: bool BindPapyrusFunctions(RE::BSScript::IVirtualMachine* vm) { // Register as a latent function: vm->RegisterLatentFunction( "StartGIFCapture", // Papyrus function name "MyScreenGrabScript", // Your .psc ScriptName StartGIFCapture // Callback above ); return true; } extern "C" DLLEXPORT bool SKSEPlugin_Load(const SKSE::LoadInterface* skse) { SKSE::Init(skse); SKSE::GetPapyrusInterface()->Register(BindPapyrusFunctions); return true; }

  • RegisterLatentFunction is the key API:

    “Registers a latent function. Unlike normal native functions, latent functions do not return a result to the script immediately. A latent function will block a script event until the latent function finishes.” (CommonLibSSE NG)

  • Your callback must return a BSScript::LatentStatus (usually kLatent) immediately, then later call vm->ReturnFromLatent(stackID, result).

3. Declaring and invoking in Papyrus

In your .psc file, you do not need any special “latent” keyword—just declare it as a normal native function:

ScriptName MyScreenGrabScript Extends Quest ; durationSeconds: how long to record (1–30) ; fps: frames per second Bool Function StartGIFCapture(Float durationSeconds, Int fps) Global Native

Then from your script you can simply call:

Event OnInit() ; This call will yield the script; the game keeps running. Bool success = StartGIFCapture(10.0, 15) If success Debug.Notification("GIF saved successfully!") Else Debug.Notification("Capture failed.") EndIf EndEvent

Because SKSE registered it as latent, Papyrus will pause at StartGIFCapture() and only continue after your C++ callback calls ReturnFromLatent.

Summary

  1. Latent functions let the game continue while your code does long‑running work (Gist, CommonLibSSE NG).

  2. Use IVirtualMachine::RegisterLatentFunction in your SKSE plugin to expose a native latent call.

  3. In your Papyrus script, declare it as a normal Native function—the engine will treat it as latent because of your C++ registration.

  4. In C++, return kLatent from your initial callback and later resume the script with ReturnFromLatent.

This pattern is exactly what you need for a multi‑frame GIF grabber: spin up your desktop‑duplication logic in C++, yield the script for the capture duration, then resume and report success once all frames are written.