sceVideoOut Implementation in SharpEmu: How the PS5 Video API Is Emulated
SharpEmu implements the PlayStation 5 sceVideoOut API as a high-level emulation (HLE) layer in the VideoOutExports class, mapping guest video ports to host Vulkan surfaces while emulating V-blank timing and buffer management in pure C#.
The sceVideoOut library (libSceVideoOut) is the core display interface used by PlayStation 5 games to render frames. In the open-source emulator SharpEmu (par274/sharpemu), this proprietary Sony API is reimplemented as managed C# code that bridges guest PS5 applications to the host PC's graphics hardware. This article examines the architecture, synchronization mechanisms, and GPU integration used to emulate video output.
Core Architecture of the sceVideoOut HLE Layer
All exported functions are defined in the static class VideoOutExports located in src/SharpEmu.Libs/VideoOut/VideoOutExports.cs. Each function is decorated with the [SysAbiExport] attribute, allowing the emulator's HLE layer to locate them by their NID (Numeric Identifier) and expose them to guest code.
Port Management and Handle Allocation
The emulator maintains a global dictionary _ports that maps numeric handles to VideoOutPortState instances. When a game calls sceVideoOutOpen, the implementation validates the bus type (typically SceVideoOutBusTypeMain) and user ID, then allocates a unique handle via _nextHandle++ before inserting a new state entry.
// sceVideoOutOpen implementation
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L199-L236
public static int sceVideoOutOpen(int userId, int busType, int index)
{
// Validation and handle allocation logic
var portState = new VideoOutPortState { ... };
_ports[handle] = portState;
return handle;
}
// sceVideoOutClose removes the entry
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L242-L252
Closing a port via sceVideoOutClose simply removes the corresponding entry from the dictionary and disposes associated resources.
Thread-Safe State Synchronization
All mutable state is guarded by the _stateGate lock object. This ensures that simultaneous guest threads cannot corrupt port data while the emulator's background V-blank pump (_vblankPumpTimer) iterates over active ports. Without this synchronization, race conditions between the guest application submitting flips and the emulator's timing thread could corrupt the VblankCount or FlipCount registers.
The V-Blank Pump and Event Signaling
A periodic Timer (approximately 16ms, simulating 60Hz) drives the PumpVblanks() method. This timer calls KernelEventQueueCompatExports.TriggerDisplayEvent for each port that has registered V-blank events via sceVideoOutAddVblankEvent.
// PumpVblanks implementation
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L68-L99
private static void PumpVblanks(object state)
{
lock (_stateGate)
{
foreach (var port in _ports.Values)
{
if (port.HasVblankEvent)
{
port.VblankCount++;
TriggerDisplayEvent(port, eventType: 0);
}
}
}
}
The pump updates a per-port VblankCount used to encode event hints for the guest application's event queue.
Buffer Registration and Pixel Format Handling
Buffer Groups and Slot Mapping
The implementation abstracts guest memory buffers into two structures: VideoOutBufferGroup (holding metadata like pixel format, tiling mode, and dimensions) and VideoOutBufferSlot (mapping specific guest addresses to group indices).
When sceVideoOutRegisterBuffers or RegisterBufferRange is called, the emulator:
- Creates a new
VideoOutBufferGroupwith the specifiedBufferAttribute - Maps guest physical addresses to
VideoOutBufferSlotentries - Updates the port's output dimensions
- Primes the
VulkanVideoPresenterto accept the images
// RegisterBufferRange implementation
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L556-L610
public static int RegisterBufferRange(int handle, ulong guestAddress, int size, ref BufferAttribute attr)
{
// Creates buffer groups and notifies Vulkan presenter
presenter.EnsureStarted(attr.Width, attr.Height);
presenter.RegisterKnownDisplayBuffers(...);
}
Pixel Format Normalization
The PS5 defines several 32-bit and 64-bit pixel formats. SharpEmu normalizes these via NormalizePixelFormat, then maps them to Vulkan guest texture formats using MapPixelFormatToGuestTextureFormat. Conversion helpers like ConvertRowToRgb handle format translation when necessary.
// Pixel-format normalization logic
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L1450-L1466
private static PixelFormat NormalizePixelFormat(uint ps5Format)
{
// Maps PS5 format constants to internal enum
}
Flip Submission and GPU Presentation
Submitting Frames with sceVideoOutSubmitFlip
When a game calls sceVideoOutSubmitFlip, the implementation verifies the buffer index, updates the port's CurrentBuffer and FlipCount, and builds an event hint for the flip event queue. If the buffer is valid, the image is forwarded to the GPU presenter.
// SubmitFlip implementation
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L540-L574
public static int sceVideoOutSubmitFlip(int handle, int bufferIndex, int flipMode, long flipArg)
{
// Validation and state update
port.CurrentBuffer = bufferIndex;
port.FlipCount++;
// Notify flip event queues
TriggerFlipEvent(port, flipArg);
// Forward to GPU presenter
vulkanPresenter.TrySubmitGuestImage(...);
}
Vulkan Integration via VulkanVideoPresenter
Actual rendering is delegated to VulkanVideoPresenter (src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs). This class bridges the software-only video-out implementation with the host GPU. During a flip, the presenter either:
- Submits a guest image via
VulkanVideoPresenter.TrySubmitGuestImage(GPU path) - Submits a host-generated RGBA frame via
SubmitHostRgbaFrame(used for internal frame capture)
The presenter creates Vulkan swap chains and manages the host window's surface, translating the emulated PS5 display pipeline into Vulkan API calls.
Debugging and Diagnostics Features
SharpEmu includes extensive diagnostics for the video-out path. When the environment variable SHARPEMU_DUMP_VIDEOOUT=1 is set, the emulator writes each submitted frame to disk as BMP or raw data. The TryDumpFrame method uses fingerprint-based duplicate suppression to avoid spamming logs with identical frames.
// Frame dumping logic
// https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs#L559-L639
private static void TryDumpFrame(VideoOutPortState port, int bufferIndex)
{
if (Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT") == "1")
{
// BMP serialization with deduplication
}
}
Complete Code Example: Guest Workflow
The following C# pseudocode mirrors the sequence a PS5 game executes to initialize video output and submit frames:
// 1. Open the main video-out port
int handle = sceVideoOutOpen(userId: 0, busType: 0, index: 0);
// 2. Configure vsync (1 = vsync, 0 = immediate, 2 = double-vsync)
sceVideoOutSetFlipRate(handle, rate: 1);
// 3. Describe buffer format (A8R8G8B8_SRGB, linear)
var attr = new BufferAttribute {
PixelFormat = 0x80000000,
TilingMode = 1, // Linear
Width = 1920,
Height = 1080,
PitchInPixel = 1920
};
sceVideoOutSetBufferAttribute(handle, ref attr);
// 4. Register guest memory buffers (double buffering)
ulong[] bufferAddresses = { 0x10000000, 0x20000000 };
sceVideoOutRegisterBuffers(handle, startIndex: 0, bufferAddresses, bufferNum: 2, ref attr);
// 5. Register for flip events (optional)
sceVideoOutAddFlipEvent(equeue, handle, userData: 0);
// 6. Submit flips during render loop
sceVideoOutSubmitFlip(handle, bufferIndex: 0, flipMode: 0, flipArg: frameCounter);
// 7. Block until next V-blank (optional)
sceVideoOutWaitVblank(handle);
// 8. Cleanup on exit
sceVideoOutClose(handle);
All calls correspond to the actual implementations in VideoOutExports.cs referenced earlier.
Summary
- Port Management: SharpEmu tracks video-out ports using a thread-safe dictionary mapping handles to
VideoOutPortStateobjects, with lifecycle managed bysceVideoOutOpenandsceVideoOutClose. - Synchronization: The
_stateGatelock protects mutable state from concurrent access by guest threads and the 60Hz V-blank pump timer. - Buffer Abstraction:
VideoOutBufferGroupandVideoOutBufferSlotstructures manage guest memory registration, pixel format normalization, and mapping to Vulkan textures. - GPU Presentation: The
VulkanVideoPresenterclass bridges the HLE layer to the host GPU, submitting guest images via Vulkan while supporting both immediate and vsync flip modes. - Debugging: Frame dumping via environment variables and extensive logging through
SharpEmuLoggerprovide visibility into the video-out pipeline.
Frequently Asked Questions
What is sceVideoOut in the context of PS5 emulation?
sceVideoOut is the PlayStation 5's native display library that manages screen output, buffer swapping, and V-sync timing. In SharpEmu, it is implemented as a high-level emulation (HLE) layer that intercepts guest函数 calls and translates them into host graphics API operations, allowing PS5 games to render without requiring actual PS5 video hardware.
How does SharpEmu handle V-sync and timing?
SharpEmu emulates V-sync using a background Timer that fires approximately every 16ms (60Hz) to simulate the hardware V-blank interrupt. The PumpVblanks method increments per-port counters and signals kernel event queues, while sceVideoOutSetFlipRate allows games to select immediate presentation, single V-sync, or double V-sync (30Hz) modes.
Where is the actual rendering performed in SharpEmu?
While the VideoOutExports class manages the emulated state and buffer metadata, actual pixel rendering occurs in VulkanVideoPresenter (VulkanVideoPresenter.cs). This class creates the host window, manages Vulkan swap chains, and either displays guest-rendered textures or host-generated frames, effectively replacing the PS5's display hardware with PC GPU capabilities.
How can developers debug video output issues in SharpEmu?
Developers can enable frame dumping by setting the environment variable SHARPEMU_DUMP_VIDEOOUT=1, which writes each submitted frame to disk in BMP format with duplicate detection. Additionally, the TraceVideoOut logging channel in SharpEmuLogger.cs provides detailed traces of every sceVideoOut function call, buffer registration, and flip submission for step-by-step debugging of the display pipeline.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →