How SharpEmu Handles Shader Submission: PS4 GCN to Vulkan SPIR-V Translation
SharpEmu processes shader submission through a three-stage pipeline: parsing PlayStation 4 AGC command buffers, translating native GCN shader binaries to SPIR-V bytecode with aggressive caching, and executing the work on a dedicated Vulkan background thread.
The SharpEmu emulator (par274/sharpemu) implements a sophisticated graphics pipeline that bridges the PlayStation 4's proprietary AGC (AMD GPU Services) driver layer to modern Vulkan APIs. Understanding how this open-source emulator handles shader submission reveals the complexity of translating proprietary GPU command streams into cross-platform GPU instructions.
The Three-Layer Shader Submission Pipeline
SharpEmu organizes its graphics pipeline into distinct architectural layers that transform native PS4 GPU commands into executable Vulkan work items.
Layer 1: AGC Driver Command Buffer Parsing
The entry point for all GPU work occurs in SharpEmu.Libs.Agc.AgcExports.cs. When an emulated game issues a draw or compute dispatch, it calls sceAgcDriverSubmitDcb (Driver Command Buffer) exports. The AgcExports.DriverSubmitDcb method reads the raw command buffer and invokes ParseSubmittedDcb to extract:
- Shader addresses (
pixelShaderAddress,vertexShaderAddress) - Texture descriptors and global memory buffers
- Render state (primitive type, vertex attribute count, viewport settings)
This HLE (High-Level Emulation) parsing decodes the PS4's proprietary command buffer format without requiring low-level hardware access.
Layer 2: Shader Translation and SPIR-V Generation
Once ParseSubmittedDcb identifies shader addresses, the system initiates binary translation. The Gen5ShaderTranslator class (SharpEmu.Libs.Agc.Gen5ShaderTranslator.cs) handles the heavy lifting:
Gen5ShaderTranslator.TryDecodeProgramreads the raw GCN (Graphics Core Next) binary from guest memory- It builds an intermediate representation of the shader logic
Gen5SpirvTranslator.TryCompile(inSharpEmu.Libs.Agc.Gen5SpirvTranslator.cs) emits standard SPIR-V bytecode compatible with Vulkan drivers
To avoid recompiling identical shaders, SharpEmu maintains several cache dictionaries:
_graphicsSpirvCache– stores translated vertex and pixel shaders_computeSpirvCache– stores compute shader translations_shaderDigests– tracks shader hashes for quick lookup
Layer 3: Vulkan Queue Submission
The final layer resides in SharpEmu.Libs.VideoOut.VulkanVideoPresenter.cs. This component receives the SPIR-V bytecode and resource bindings, then manages the actual GPU submission through three primary methods:
SubmitTranslatedDraw– for rasterized graphics (vertex + pixel shader pairs)SubmitComputeDispatch– for compute workloadsTrySubmitGuestImage– for raw image presentation (used bysceVideoOutSubmitFlip)
Detailed Execution Flow from PS4 Binary to GPU
The complete shader submission lifecycle follows a precise path through SharpEmu's architecture:
-
Game submission – The emulated title calls
sceAgcDriverSubmitDcbwith a command buffer containing draw or compute instructions. -
Command parsing –
AgcExports.ParseSubmittedDcbdecodes the opcode to determine if this is a raster draw or compute dispatch, extracting shader addresses and texture lists. -
Shader lookup – The system checks
_graphicsSpirvCacheor_computeSpirvCachefor existing translations. Cache misses trigger theGen5ShaderTranslator→Gen5SpirvTranslatorpipeline. -
Work item creation – For raster draws, the system creates a
VulkanTranslatedGuestDrawobject containing the SPIR-V shaders, texture handles, and viewport dimensions. Compute dispatches useVulkanComputeGuestDispatch. -
Queue insertion – The work item is enqueued in
_enqueuedGuestWorkunder the protection of a global_gatelock to ensure thread safety. -
Background execution – A dedicated background thread (
Runmethod) continuously drains the queue, constructs VulkanPipelineShaderStageCreateInfostructures, binds descriptor sets for textures and uniform buffers, and issues the finalvkQueueSubmitcall.
Code Examples
Submitting a Raster Draw (Vertex + Pixel Shaders)
When handling standard 3D rendering, SharpEmu submits both vertex and pixel shader stages:
// SPIR-V bytecode obtained from Gen5SpirvTranslator
byte[] vertexSpirv = GetCachedOrTranslateShader(vertexAddr);
byte[] pixelSpirv = GetCachedOrTranslateShader(pixelAddr);
// Define texture inputs
var textures = new List<VulkanGuestDrawTexture> {
new VulkanGuestDrawTexture(
address: 0x1234_0000,
width: 256,
height: 256,
format: 12,
isStorage: false)
};
// Submit to Vulkan layer
VulkanVideoPresenter.SubmitTranslatedDraw(
pixelSpirv: pixelSpirv,
textures: textures,
globalMemoryBuffers: new List<VulkanGuestMemoryBuffer>(),
width: 1920,
height: 1080,
attributeCount: 3,
vertexSpirv: vertexSpirv,
vertexCount: 3,
instanceCount: 1,
primitiveType: 4); // 4 == TRIANGLE_LIST
Submitting a Compute Shader Dispatch
Compute workloads follow a similar path but use different submission parameters:
ulong shaderAddr = 0xABCD_0000; // Original GCN shader address
byte[] computeSpirv = TranslateComputeShader(shaderAddr);
var textures = new List<VulkanGuestDrawTexture> {
new VulkanGuestDrawTexture(
address: 0x2000_0000,
width: 64,
height: 64,
format: 12,
isStorage: true) // Storage image for compute write
};
VulkanVideoPresenter.SubmitComputeDispatch(
shaderAddress: shaderAddr,
computeSpirv: computeSpirv,
textures: textures,
globalMemoryBuffers: new List<VulkanGuestMemoryBuffer>(),
groupCountX: 8,
groupCountY: 8,
groupCountZ: 1);
Submitting a Raw Image Flip
For presenting rendered frames to the display output:
ulong guestImage = 0x3000_0000;
bool submitted = VulkanVideoPresenter.TrySubmitGuestImage(
address: guestImage,
width: 1280,
height: 720,
pitchInPixel: 0);
if (!submitted) {
Logger.Warning("[LOADER][WARN] Failed to submit guest image flip");
}
Shader Caching Strategy
SharpEmu implements aggressive caching to minimize translation overhead. The _graphicsSpirvCache and _computeSpirvCache dictionaries map original PS4 shader addresses to compiled SPIR-V byte arrays. The _shaderDigests collection maintains hashes of shader binaries to detect identical shaders across different command buffers.
This caching proves critical for performance, as GCN-to-SPIR-V translation involves expensive analysis and optimization passes. By storing the final Vulkan-ready bytecode, subsequent draws using identical shaders execute with minimal overhead.
Threading and Queue Management
The VulkanVideoPresenter uses a producer-consumer pattern to decouple emulation timing from GPU execution. The main emulation thread (producer) parses commands and generates VulkanTranslatedGuestDraw or VulkanComputeGuestDispatch objects, pushing them to _enqueuedGuestWork.
A dedicated background thread (consumer) runs the Run method, which:
- Locks the
_gatemutex - Dequeues pending work items
- Builds Vulkan command buffers with
PipelineShaderStageCreateInfofor each shader stage - Binds descriptor sets referencing the original PS4 texture addresses
- Calls
vkQueueSubmitto execute on the physical GPU
This architecture prevents emulation stalls during GPU pipeline flushes or shader compilation, maintaining smooth frame rates even when processing complex shader graphs.
Summary
- Three-stage pipeline – SharpEmu parses AGC command buffers in
AgcExports.cs, translates GCN binaries to SPIR-V viaGen5ShaderTranslatorandGen5SpirvTranslator, and submits to Vulkan throughVulkanVideoPresenter. - Aggressive caching – The
_graphicsSpirvCacheand_computeSpirvCachedictionaries eliminate redundant shader recompilation. - Thread-safe submission – Background thread processing via
_enqueuedGuestWorkensures emulation and GPU execution remain decoupled. - Three submission types –
SubmitTranslatedDrawfor raster graphics,SubmitComputeDispatchfor compute workloads, andTrySubmitGuestImagefor display presentation.
Frequently Asked Questions
How does SharpEmu translate PS4 shaders to run on PC GPUs?
SharpEmu translates shaders using the Gen5ShaderTranslator class to decode the raw GCN binary into an intermediate representation, then Gen5SpirvTranslator compiles this to standard SPIR-V bytecode that any Vulkan-compatible GPU can execute. This translation happens transparently when ParseSubmittedDcb encounters a new shader address not present in the _graphicsSpirvCache.
What is the difference between SubmitTranslatedDraw and SubmitComputeDispatch?
SubmitTranslatedDraw handles rasterized graphics by accepting both vertex and pixel shader SPIR-V code along with primitive topology information, while SubmitComputeDispatch handles general-purpose GPU compute workloads using only a compute shader and dispatch dimensions (group counts). The former creates VulkanTranslatedGuestDraw objects, while the latter creates VulkanComputeGuestDispatch instances for the work queue.
Does SharpEmu cache translated shaders between frames?
Yes. SharpEmu maintains _graphicsSpirvCache for vertex and pixel shaders and _computeSpirvCache for compute shaders. These dictionaries map the original PS4 shader addresses to compiled SPIR-V byte arrays. The system also uses _shaderDigests to identify identical shaders by hash, preventing redundant translation even when the same shader appears at different memory addresses.
Where does the actual GPU execution happen in SharpEmu's code?
The actual Vulkan submission occurs in VulkanVideoPresenter.cs within the background thread's Run method. This method constructs Vulkan command buffers, creates pipeline stages using PipelineShaderStageCreateInfo, binds descriptor sets for textures, and issues the final vkQueueSubmit call to the GPU driver. The public entry points SubmitTranslatedDraw and SubmitComputeDispatch merely enqueue work for this background thread.
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 →