Skip to main content

Overview

The EmulationContext class is the heart of the Hydra emulator, managing the entire emulation lifecycle including CPU, GPU, audio, and the Horizon OS environment. It provides the interface between frontends (SwiftUI, SDL3, etc.) and the emulation core.

Class Definition

namespace hydra {

class EmulationContext {
public:
    EmulationContext(horizon::ui::HandlerBase& ui_handler);
    ~EmulationContext();

    void SetSurface(void* surface);
    
    enum class LoadAndStartError {
        ProcessAlreadyExists,
    };
    
    void LoadAndStart(horizon::loader::LoaderBase* loader);
    void RequestStop();
    void ForceStop();
    
    void Pause();
    void Resume();
    
    void NotifyOperationModeChanged();
    void ProgressFrame(u32 width, u32 height, bool& out_dt_average_updated);
    
    bool IsRunning() const;
    f32 GetLastDeltaTimeAverage() const;
    
    void TakeScreenshot();
    void CaptureGpuFrame();
};

}
Defined in src/core/emulation_context.hpp

Initialization

Constructor

Creates a new emulation context.
EmulationContext(horizon::ui::HandlerBase& ui_handler);
ui_handler
horizon::ui::HandlerBase&
required
UI handler for frontend integration (handles input, UI events, etc.)
The constructor initializes:
  • CPU backend (Hypervisor or Dynarmic based on configuration)
  • GPU renderer (Metal)
  • Audio core (Cubeb or Null)
  • Horizon OS environment
Example:
class MyUIHandler : public hydra::horizon::ui::HandlerBase {
    // Implement UI handler interface
};

MyUIHandler ui_handler;
hydra::EmulationContext emulation(ui_handler);

Destructor

Cleans up all emulation resources.
~EmulationContext();
Automatically called when the context is destroyed. Stops emulation if running and frees CPU, GPU, audio, and OS resources.

Surface Management

SetSurface

Sets the rendering surface for GPU output.
void SetSurface(void* surface);
surface
void*
required
Platform-specific surface pointer. On Metal, this is a CAMetalLayer*.
Must be called before starting emulation to provide a rendering target. SwiftUI Example:
struct MetalView: NSViewRepresentable {
    let context: UnsafeMutableRawPointer
    
    func makeNSView(context: Context) -> some NSView {
        let view = NSView()
        let layer = CAMetalLayer()
        view.layer = layer
        view.wantsLayer = true
        
        hydra_emulation_context_set_surface(self.context, layer)
        
        return view
    }
}

Loading and Execution

LoadAndStart

Loads a game and starts emulation.
void LoadAndStart(horizon::loader::LoaderBase* loader);
loader
horizon::loader::LoaderBase*
required
Loader containing the game to execute (NSP, XCI, NCA, etc.)
This function:
  1. Creates a new Horizon OS process
  2. Loads the game executable and data
  3. Applies any available patches
  4. Starts CPU execution
  5. Begins the emulation loop
Calling LoadAndStart when a process is already running will result in a LoadAndStartError::ProcessAlreadyExists error.
Example:
auto loader = hydra::horizon::loader::LoaderBase::Create("/path/to/game.nsp");
if (loader) {
    try {
        emulation.LoadAndStart(loader.get());
    } catch (const std::exception& e) {
        // Handle loading error
    }
}

C API Example

void* loader = hydra_create_loader_from_path(
    (hydra_string){"/path/to/game.nsp", 18}
);

if (loader) {
    hydra_emulation_context_load_and_start(ctx, loader);
    hydra_loader_destroy(loader);
}

Execution Control

Pause

Pauses emulation.
void Pause();
Suspends CPU execution, GPU rendering, and audio output. The emulation state is preserved and can be resumed.

Resume

Resumes paused emulation.
void Resume();
Continues execution from where it was paused. Example:
// Pause when app goes to background
emulation.Pause();

// Resume when app returns to foreground
emulation.Resume();

RequestStop

Requests a graceful shutdown of emulation.
void RequestStop();
Signals the emulation thread to stop. This allows the emulator to:
  • Flush save data
  • Clean up resources properly
  • Exit gracefully
The emulation may take a few frames to fully stop.

ForceStop

Immediately stops emulation.
void ForceStop();
Force stopping may cause data loss if save data hasn’t been flushed. Use RequestStop() when possible.

IsRunning

Checks if emulation is currently running.
bool IsRunning() const;
return
bool
true if a game is loaded and executing, false otherwise
Example:
while (emulation.IsRunning()) {
    // Process frames
    bool dt_updated = false;
    emulation.ProgressFrame(1920, 1080, dt_updated);
    
    if (dt_updated) {
        float fps = 1.0f / emulation.GetLastDeltaTimeAverage();
        printf("FPS: %.1f\n", fps);
    }
}

Frame Processing

ProgressFrame

Advances emulation by one frame.
void ProgressFrame(u32 width, u32 height, bool& out_dt_average_updated);
width
u32
required
Current display width in pixels
height
u32
required
Current display height in pixels
out_dt_average_updated
bool&
required
Output parameter set to true if the delta time average was recalculated this frame
This function:
  1. Executes CPU instructions
  2. Processes GPU commands
  3. Renders the frame to the surface
  4. Updates timing metrics
Typically called once per display refresh (60 Hz). Example:
void renderLoop() {
    while (emulation.IsRunning()) {
        bool dt_updated = false;
        emulation.ProgressFrame(1920, 1080, dt_updated);
        
        // Frame is now rendered to the surface
    }
}

GetLastDeltaTimeAverage

Gets the average frame time.
f32 GetLastDeltaTimeAverage() const;
return
f32
Average delta time in seconds per frame
Useful for calculating FPS and performance metrics:
float avg_frame_time = emulation.GetLastDeltaTimeAverage();
float fps = 1.0f / avg_frame_time;

System Events

NotifyOperationModeChanged

Notifies the emulator that the operation mode changed.
void NotifyOperationModeChanged();
Call this when switching between handheld and docked mode. The emulator will:
  • Adjust resolution (720p handheld / 1080p docked)
  • Modify CPU/GPU performance profiles
  • Update the Horizon OS system state
Example:
// User switched to docked mode
config.GetHandheldMode() = false;
config.Serialize();
emulation.NotifyOperationModeChanged();

Debugging and Capture

TakeScreenshot

Captures the current frame as a screenshot.
void TakeScreenshot();
Screenshots are saved to the pictures directory (typically {app_data}/pictures). Example:
// User pressed screenshot hotkey
emulation.TakeScreenshot();

CaptureGpuFrame

Captures detailed GPU frame data for debugging.
void CaptureGpuFrame();
This captures:
  • GPU command buffers
  • Shader code
  • Texture data
  • Render targets
Useful for debugging graphics issues.

Internal Components

The EmulationContext manages several subsystems:
private:
    hw::tegra_x1::cpu::ICpu* cpu;        // CPU backend
    hw::tegra_x1::gpu::Gpu* gpu;          // GPU renderer
    audio::ICore* audio_core;             // Audio subsystem
    horizon::OS* os;                      // Horizon OS environment
    horizon::kernel::Process* process;    // Active game process

CPU Backend

Either Apple Hypervisor (native ARM64) or Dynarmic (JIT compiler) based on configuration.

GPU Renderer

Metal-based GPU emulation with shader translation from Switch GPU code.

Audio Core

Cubeb-based audio output or null audio for headless operation.

Horizon OS

Complete Nintendo Switch OS environment including:
  • Kernel services
  • System modules
  • Save data management
  • Account system

Integration Examples

SwiftUI Frontend

class EmulationViewModel: ObservableObject {
    private var context: UnsafeMutableRawPointer?
    @Published var isRunning = false
    @Published var fps: Float = 0.0
    
    func loadGame(path: String) {
        context = hydra_create_emulation_context()
        
        path.withHydraString { hydraPath in
            let loader = hydra_create_loader_from_path(hydraPath)
            defer { hydra_loader_destroy(loader) }
            
            hydra_emulation_context_load_and_start(context, loader)
            isRunning = true
        }
    }
    
    func pause() {
        hydra_emulation_context_pause(context)
        isRunning = false
    }
    
    func resume() {
        hydra_emulation_context_resume(context)
        isRunning = true
    }
    
    func progressFrame(width: Int, height: Int) {
        var dtUpdated = false
        hydra_emulation_context_progress_frame(
            context,
            UInt32(width),
            UInt32(height),
            &dtUpdated
        )
        
        if dtUpdated {
            let avgDt = hydra_emulation_context_get_last_delta_time_average(context)
            fps = 1.0 / avgDt
        }
    }
    
    deinit {
        if let ctx = context {
            hydra_emulation_context_destroy(ctx)
        }
    }
}

SDL3 Frontend

class SDL3Frontend {
    hydra::EmulationContext emulation;
    SDL_Window* window;
    bool running = true;
    
public:
    SDL3Frontend() : emulation(ui_handler) {
        window = SDL_CreateWindow(
            "Hydra",
            1920, 1080,
            SDL_WINDOW_METAL
        );
        
        // Set Metal surface
        void* metal_layer = SDL_Metal_GetLayer(
            SDL_Metal_CreateView(window)
        );
        emulation.SetSurface(metal_layer);
    }
    
    void run(const std::string& game_path) {
        auto loader = hydra::horizon::loader::LoaderBase::Create(game_path);
        emulation.LoadAndStart(loader.get());
        
        while (running && emulation.IsRunning()) {
            processEvents();
            
            int width, height;
            SDL_GetWindowSize(window, &width, &height);
            
            bool dt_updated = false;
            emulation.ProgressFrame(width, height, dt_updated);
            
            SDL_Metal_PresentFrame();
        }
    }
    
    void processEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT) {
                running = false;
                emulation.RequestStop();
            }
        }
    }
};
See src/frontend/swiftui/ and src/frontend/sdl3/ for complete frontend implementations.

Error Handling

The EmulationContext uses exceptions for error handling:
try {
    emulation.LoadAndStart(loader);
} catch (const std::runtime_error& e) {
    if (std::string(e.what()).find("ProcessAlreadyExists") != std::string::npos) {
        // Handle duplicate process error
        emulation.RequestStop();
        // Wait for stop then retry
    } else {
        // Handle other errors
    }
}

Thread Safety

The EmulationContext is not thread-safe. All methods must be called from the same thread (typically the main emulation thread).
If you need to interact with the emulator from multiple threads:
  • Use message queues or command buffers
  • Implement proper synchronization
  • Consider using the debugger API which provides explicit locking

Performance Considerations

  • Call ProgressFrame() at the display refresh rate (60 Hz)
  • Avoid blocking operations in the emulation thread
  • Use Pause() when the app is backgrounded to save resources
  • Monitor GetLastDeltaTimeAverage() to detect performance issues

See Also

Build docs developers (and LLMs) love