How SharpEmu Implements AMPR Exports: PS5 libSceAmpr Emulation Guide

SharpEmu implements AMPR exports as annotated HLE functions in AmprExports.cs using a command-buffer model that queues file reads and kernel events before processing them in bulk via CompleteCommandBuffer.

SharpEmu is a PlayStation 5 emulator that provides high-level emulation (HLE) of the libSceAmpr library through a set of exported functions decorated with [SysAbiExport]. The implementation centers on a command-buffer model that batches asynchronous file I/O and kernel event queue operations. All AMPR exports are defined in src/SharpEmu.Libs/Ampr/AmprExports.cs and interact with supporting registries for file path mapping and PACK archive handling.

Understanding the AMPR Command-Buffer Architecture

The SharpEmu AMPR implementation revolves around a command-buffer model that mimics the PlayStation 5 ABI. This design allows the guest to queue multiple operations—file reads, kernel event notifications, and completion addresses—before dispatching them atomically.

The Core Components

The architecture relies on four primary data structures managed within AmprExports.cs:

  • _commandBuffers – A thread-safe ConcurrentDictionary that maps a guest buffer address to its runtime state, tracking the buffer, its size, and the current write offset.
  • _hostFileCache – A cache of opened host files that minimizes file-open overhead when the guest repeatedly reads the same file.
  • PakDirectoryTracker – Handles sequential reads of PACK archives using the "-1" offset protocol to infer the next logical chunk.
  • AmprFileRegistry – Maps guest paths to host paths and supplies deterministic file IDs using FNV-1a hashing.

Command Buffer Lifecycle

Every command buffer passes through three distinct phases. First, the guest calls sceAmprCommandBufferConstructor to initialize the buffer state. Then, the guest appends records—such as read requests or kernel event queue entries—using various append methods. Finally, the guest calls sceAmprCompleteCommandBuffer (internal logic at lines 69-127 in AmprExports.cs) to walk the buffer, dispatch each record, and advance the write offset.

Implementing the AMPR Export Functions

All exported functions in SharpEmu follow a consistent pattern: validate arguments, manipulate command-buffer state via helper methods, optionally trace the operation, and return an OrbisGen2Result code matching the PS5 ABI.

Command Buffer Management

The constructor and destructor exports manage the lifecycle of command buffers in the _commandBuffers dictionary.

sceAmprCommandBufferConstructor (lines 63-82 in AmprExports.cs) creates a new command buffer and optionally clears it. The corresponding destructor, sceAmprCommandBufferDestructor (lines 143-161), clears the buffer and removes it from the internal dictionary. For asynchronous buffers, sceAmprAprCommandBufferConstructor (lines 90-109) provides the same initialization logic under the APR (Audio-Path-Reader) namespace.

// Example registration in the internal dictionary
_commandBuffers[bufferAddress] = new CommandBufferState 
{
    Buffer = bufferPtr,
    Size = bufferSize,
    CurrentOffset = 0
};

File I/O Operations and Registry

The sceAmprCommandBufferReadFile export (implemented in AprCommandBufferReadFile at lines 54-124) reads a host file into guest memory. This function handles missing files, sequential reads, and PACK archives by consulting the AmprFileRegistry to resolve the file ID to a host path.

File IDs are generated using FNV-1a hashing in AmprFileRegistry.ComputeFileId. The registry maintains a ConcurrentDictionary mapping fileId → hostPath, allowing constant-time lookups during read operations.

// From AmprFileRegistry.cs
uint fileId = AmprFileRegistry.Register("/guest/file.bin", "C:\\host\\file.bin");
if (AmprFileRegistry.TryGetHostPath(fileId, out var hostPath)) 
{
    // Proceed with file read
}

Kernel Event Queue Integration

AMPR supports asynchronous notifications through the kernel event queue. The sceAmprCommandBufferWriteKernelEventQueue_04_00 export (lines 14-40 in AmprExports.cs) appends a kernel-event-queue record to the command buffer. This integrates with KernelEventQueueCompatExports.cs to signal completion events to the guest kernel.

Additionally, sceAmprCommandBufferWriteAddressOnCompletion (lines 48-66) appends a write-address-on-completion record, allowing the guest to specify memory locations that should be updated when the buffer completes.

Handling PACK Archives and Sequential Reads

SharpEmu handles sequential reads for PACK archives through the PakDirectoryTracker class. When the guest supplies -1 as the file offset, the emulator infers the next logical chunk by tracking per-file state and parsing directory tables.

Key logic in PakDirectoryTracker.cs resolves the sequential offset:

public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
{
    var state = _state.GetOrAdd(fileId, _ => new FileState());

    // If directory has been parsed, match entry by size
    if (state.Directory is { } entries)
    {
        foreach (var entry in entries)
        {
            if (!entry.Consumed && entry.FileLen == requestedSize) 
            { 
                entry.Consumed = true;
                return entry.FilePos; 
            }
        }
    }

    // Fallback to linear cursor
    return state.NextOffset;
}

The OnReadCompleted method updates the cursor, parses headers, or records the directory once the read operation passes the header boundary.

Debugging AMPR Operations with Environment Variables

SharpEmu provides detailed tracing for AMPR operations through environment variables. Setting SHARPEMU_LOG_AMPR=1 or SHARPEMU_LOG_AMPR_READS=1 enables diagnostic output to stderr.

The tracing helpers prepend [LOADER][TRACE] ampr.* for easy filtering:

if (_traceAmpr) 
{
    Console.Error.WriteLine($"[LOADER][TRACE] ampr.{operation}: cmd=0x{commandBuffer:X16}, fileId={fileId}");
}

This is particularly useful for debugging file path resolution issues or verifying that PACK archive sequential reads are advancing correctly.

Code Example: Typical Guest Usage Pattern

The following pattern demonstrates how guest code typically interacts with SharpEmu's AMPR exports:

// 1. Register a host file (done once on startup)
uint fileId = sceAmprRegisterFile("/game/data.bin", "C:/Games/PS5/data.bin");

// 2. Allocate a command buffer in guest memory (size 0x1000)
void* cmdBuf = malloc(0x1000);
sceAmprCommandBufferConstructor(&cmdBuf, cmdBuf, 0x1000);

// 3. Queue a read of the first 0x200 bytes (sequential offset = -1)
sceAmprAprCommandBufferReadFile(cmdBuf, fileId, dstPtr, 0x200);

// 4. Queue a kernel-event-queue notification
sceAmprCommandBufferWriteKernelEventQueue_04_00(cmdBuf, queueHandle, ident, token, userdata);

// 5. Complete the buffer – driver processes all queued records
sceAmprCompleteCommandBuffer(cmdBuf);

Summary

  • SharpEmu AMPR exports are implemented in AmprExports.cs using [SysAbiExport] attributes to register functions with the HLE dispatcher.
  • The command-buffer model batches operations in a guest-allocated structure, processing them atomically via CompleteCommandBuffer.
  • File identification uses FNV-1a hashing through AmprFileRegistry to map guest paths to host paths with deterministic IDs.
  • PACK archive support relies on PakDirectoryTracker to handle sequential reads when the guest specifies -1 as the file offset.
  • Debugging is enabled via the SHARPEMU_LOG_AMPR environment variable, which outputs detailed trace information to stderr.

Frequently Asked Questions

What is the purpose of the command buffer in SharpEmu's AMPR implementation?

The command buffer serves as a queue for asynchronous operations. It allows the guest to append multiple file read requests and kernel event notifications before dispatching them all at once through sceAmprCompleteCommandBuffer. This batching approach mimics the PlayStation 5's libSceAmpr ABI and reduces the overhead of individual system calls.

How does SharpEmu handle file identification for AMPR exports?

SharpEmu uses the AmprFileRegistry class to map guest file paths to host file paths. When a file is registered, the system computes a deterministic file ID using the FNV-1a hash algorithm. This ID is used throughout the AMPR system to reference files without passing string paths, enabling efficient lookups in the _hostFileCache and PakDirectoryTracker.

What is the difference between sceAmprCommandBufferConstructor and sceAmprAprCommandBufferConstructor?

Both functions initialize command buffers, but they serve different namespaces in the PlayStation 5 ABI. sceAmprCommandBufferConstructor (lines 63-82) handles standard AMPR buffers, while sceAmprAprCommandBufferConstructor (lines 90-109) specifically handles "APR" (Audio-Path-Reader) buffers. In SharpEmu's implementation, both methods perform identical initialization logic but are exported as separate symbols to match the PS5 library's expected interface.

How can I debug AMPR file read operations in SharpEmu?

Set the environment variable SHARPEMU_LOG_AMPR=1 or SHARPEMU_LOG_AMPR_READS=1 before running the emulator. This enables detailed tracing that outputs to stderr with the prefix [LOADER][TRACE] ampr.*, showing each file read operation, command buffer address, file ID, and offset. This is particularly useful for verifying that PakDirectoryTracker is correctly resolving sequential offsets for PACK archives.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →