Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/azahar-emu/azahar/llms.txt

Use this file to discover all available pages before exploring further.

The 3DS graphics pipeline is driven by a proprietary GPU called the PICA200, which implements a fixed-function vertex and fragment pipeline unlike any standard graphics API. Azahar emulates the PICA200 through a layered design: VideoCore::GPU is the high-level interface to the core, Pica::PicaCore holds all PICA200 register state and command processing, and a pluggable VideoCore::RendererBase translates that state to the host GPU.

Component overview

GPU (high-level interface)

VideoCore::GPU (src/video_core/gpu.h) receives GSP commands from the HLE service layer, executes memory fills and DMA transfers, and signals VBlank interrupts on a timer tied to the 3DS frame rate (4,481,136 ARM11 cycles per frame).

PICA200 core

Pica::PicaCore (in src/video_core/pica/) holds the full PICA200 register file and processes command lists submitted by the game. All renderer backends read from this shared register state.

Renderer base

VideoCore::RendererBase (src/video_core/renderer_base.h) is the abstract renderer interface. Backends inherit from it and implement SwapBuffers(), TryPresent(), and Rasterizer().

Rasterizer cache

src/video_core/rasterizer_cache/ provides surface and texture caching shared across the accelerated backends. It tracks GPU memory writes and invalidates or flushes cached surfaces when the game modifies them.

The GPU class

VideoCore::GPU is created inside Core::System and owned by the std::unique_ptr<VideoCore::GPU> gpu member. It exposes the interface that the HLE GSP service (service/gsp/gsp_gpu.h) calls to submit work:
// src/video_core/gpu.h
class GPU {
public:
    void SetInterruptHandler(Service::GSP::InterruptHandler handler);
    void FlushRegion(PAddr addr, u32 size);
    void InvalidateRegion(PAddr addr, u32 size);
    void ClearAll(bool flush);
    void Execute(const Service::GSP::Command& command);
    void SetBufferSwap(u32 screen_id, const Service::GSP::FrameBufferInfo& info);
    void WriteReg(VAddr addr, u32 data);

    [[nodiscard]] VideoCore::RendererBase& Renderer();
    [[nodiscard]] Pica::PicaCore& PicaCore();
    [[nodiscard]] Pica::DebugContext& DebugContext();

    RightEyeDisabler& GetRightEyeDisabler();
    // ...
};
VBlank timing is driven by VBlankCallback, which fires every FRAME_TICKS (4,481,136) ARM11 clock cycles — the value measured on real hardware.

Renderer backends

VideoCore::CreateRenderer() (in src/video_core/video_core.h) inspects the active graphics API setting and constructs the appropriate RendererBase subclass:
// src/video_core/video_core.h
std::unique_ptr<RendererBase> CreateRenderer(
    Frontend::EmuWindow& emu_window,
    Frontend::EmuWindow* secondary_window,
    Pica::PicaCore& pica,
    Core::System& system);
Three backends ship with Azahar:

OpenGL

src/video_core/renderer_opengl/ — The primary accelerated backend. Uses OpenGL 4.3 core profile. Supports resolution upscaling, anisotropic filtering, texture filtering, and disk shader caching.

Vulkan

src/video_core/renderer_vulkan/ — The secondary accelerated backend. Targets Vulkan 1.1+. Provides the same feature set as the OpenGL backend and is preferred on platforms where OpenGL performance is poor.

Software

src/video_core/renderer_software/ — A fully CPU-bound rasterizer that does not require any GPU driver support. Much slower than the accelerated backends; useful for debugging and reference comparison.
RendererBase exposes the common interface both backends implement:
// src/video_core/renderer_base.h
class RendererBase : NonCopyable {
public:
    virtual VideoCore::RasterizerInterface* Rasterizer() = 0;
    virtual void SwapBuffers() = 0;
    virtual void TryPresent(int timeout_ms, bool is_secondary) = 0;
    virtual void PrepareVideoDumping() {}
    virtual void CleanupVideoDumping() {}
    virtual void NotifySurfaceChanged(bool second) {}

    u32  GetResolutionScaleFactor();
    void RequestScreenshot(void* data,
                           std::function<void(bool)> callback,
                           const Layout::FramebufferLayout& layout);
    // ...
};
RendererSettings (in renderer_base.h) carries atomic flags such as screenshot_requested, bg_color_update_requested, and shader_update_requested that the frontend thread can set safely while the render thread is running.

PICA200 shader emulation

The PICA200 has a proprietary programmable vertex shader (called a “geometry shader” in Nintendo’s documentation) that does not map to GLSL or SPIR-V directly. Azahar emulates it in src/video_core/shader/:
FileDescription
shader.h / shader.cppShader unit state — registers, output map, uniform data
shader_interpreter.h / shader_interpreter.cppInterprets PICA shader opcodes one instruction at a time. Always available.
shader_jit.h / shader_jit.cppDispatcher that selects the JIT compiler based on host architecture
shader_jit_x64_compiler.h / .cppJIT compiler targeting x86-64 using Dynarmic’s assembler
shader_jit_a64_compiler.h / .cppJIT compiler targeting AArch64
generator/GLSL source generator — produces GLSL vertex shaders from PICA shader binaries for the OpenGL and Vulkan backends
On x86-64 and arm64, the JIT shader compiler is used by default because it is significantly faster than the interpreter. The interpreter is always available as a fallback and is used by the software renderer.

Rasterizer cache

src/video_core/rasterizer_cache/ implements surface and texture caching for the accelerated backends. Key types:
FileClass / purpose
rasterizer_cache.hTemplate RasterizerCache<Traits> — the main cache, parameterized per backend
surface_base.h / surface_params.hSurfaceBase, SurfaceParams — describe a cached surface: format, dimensions, level count
framebuffer_base.hFramebufferBase — cached framebuffer objects
texture_codec.hMorton-order swizzle/deswizzle and pixel format conversions for PICA texture data
pixel_format.hEnumeration of all PICA pixel formats and their host equivalents
VideoCore::GPU::FlushRegion() and InvalidateRegion() are the two entry points that keep the cache coherent when the CPU writes into GPU-mapped memory.

Custom textures

VideoCore::CustomTexManager (src/video_core/custom_textures/custom_tex_manager.h) manages texture replacement packs. When a game samples a texture, the rasterizer cache computes a hash of the original pixel data and checks whether a replacement exists:
// src/video_core/custom_textures/custom_tex_manager.h
class CustomTexManager {
public:
    void FindCustomTextures();
    bool ReadConfig(u64 title_id, bool options_only = false);
    void PreloadTextures(const std::atomic_bool& stop_run,
                         const VideoCore::DiskResourceLoadCallback& callback);
    void DumpTexture(const SurfaceParams& params, u32 level,
                     std::span<u8> data, u64 data_hash);
    Material* GetMaterial(u64 data_hash);
    bool Decode(Material* material, std::function<bool()>&& upload);
    // ...
};
Replacement textures are stored under the user data directory in a folder named after the title ID. Material (material.h) represents a single replacement asset, which may contain multiple mip levels. Async uploads are queued in async_uploads and drained during TickFrame() to avoid stalling the render thread.
You can dump the original textures by enabling the texture dump option in settings. Dumped textures are saved as PNG files named by their data hash, which you can then replace with higher-resolution artwork and redistribute as a texture pack.

Stereoscopic 3D

The 3DS top screen can display stereoscopic 3D content by rendering a separate right-eye frame. Azahar handles this through VideoCore::RightEyeDisabler (src/video_core/right_eye_disabler.h):
// src/video_core/right_eye_disabler.h
class RightEyeDisabler {
public:
    bool ShouldAllowCmdQueueTrigger(PAddr addr, u32 size);
    bool ShouldAllowDisplayTransfer(PAddr src_address, size_t size);
    void ReportEndFrame();
    void SetEnabled(bool enable);
};
GPU owns a std::unique_ptr<RightEyeDisabler> and exposes it via GPU::GetRightEyeDisabler(). When stereoscopic output is disabled, RightEyeDisabler intercepts GSP command-queue triggers and display-transfer commands that correspond to the right-eye frame, dropping them before they reach the rasterizer. This saves roughly half the GPU work for games that render both eyes.

Host shaders

src/video_core/host_shaders/ contains the GLSL and SPIR-V (via glslang) shaders that Azahar’s OpenGL and Vulkan renderers use for presentation and post-processing:
Shader filePurpose
opengl_present.vert / .fragStandard screen blit for the OpenGL backend
opengl_present_anaglyph.fragRed-cyan anaglyph 3D output (OpenGL)
opengl_present_interlaced.fragInterlaced stereoscopic output (OpenGL)
vulkan_present.vert / .fragStandard screen blit for the Vulkan backend
vulkan_present_anaglyph.fragRed-cyan anaglyph 3D output (Vulkan)
vulkan_present_interlaced.fragInterlaced stereoscopic output (Vulkan)
vulkan_depth_to_buffer.compCompute shader for reading back depth values
format_reinterpreter/Pixel-format conversion shaders (e.g., D24S8 → RGBA8)
texture_filtering/Upscaling filter shaders (xBRZ, bicubic, etc.)
Shaders are embedded into the binary at compile time via the source_shader.h.in template and the StringShaderHeader.cmake helper. At runtime the renderer compiles them with the host driver.

Build docs developers (and LLMs) love