diff --git a/.github/workflows/pr-build-links.yml b/.github/workflows/pr-build-links.yml
new file mode 100644
index 0000000..3a5b978
--- /dev/null
+++ b/.github/workflows/pr-build-links.yml
@@ -0,0 +1,105 @@
+# Copyright (C) 2026 SharpEmu Emulator Project
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Posts (and keeps updated) a single PR comment linking the per-platform build
+# artifacts once "Build and Release" finishes.
+#
+# This is a workflow_run workflow on purpose: PRs from forks run "Build and
+# Release" with a read-only GITHUB_TOKEN and cannot comment. workflow_run runs
+# afterwards in the base-repo context with a read-write token and does not check
+# out untrusted fork code, so it can comment safely. Because of that, GitHub only
+# triggers it from the copy on the default branch — it does nothing until merged
+# to main.
+name: PR Build Links
+
+on:
+ workflow_run:
+ workflows: ["Build and Release"]
+ types:
+ - completed
+
+permissions:
+ contents: read
+ actions: read
+ pull-requests: write
+
+jobs:
+ comment:
+ name: Post artifact links
+ runs-on: ubuntu-latest
+ # Only for successful PR builds — artifacts exist only when the build passed.
+ if: >-
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success'
+ steps:
+ - name: Post or update the artifact-links comment
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const run = context.payload.workflow_run;
+ const { owner, repo } = context.repo;
+
+ // Fork PRs leave workflow_run.pull_requests empty, so fall back to
+ // resolving the PR from the build's head commit.
+ let prNumber;
+ if (run.pull_requests && run.pull_requests.length > 0) {
+ prNumber = run.pull_requests[0].number;
+ } else {
+ const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner, repo, commit_sha: run.head_sha,
+ });
+ const open = prs.data.find(pr => pr.state === 'open');
+ if (!open) {
+ core.info('No open PR for this build; nothing to comment.');
+ return;
+ }
+ prNumber = open.number;
+ }
+
+ // Collect the per-platform artifacts the build produced.
+ const artifacts = await github.paginate(
+ github.rest.actions.listWorkflowRunArtifacts,
+ { owner, repo, run_id: run.id, per_page: 100 },
+ );
+ const platforms = [
+ { key: 'win-x64', label: 'Windows (win-x64)' },
+ { key: 'linux-x64', label: 'Linux (linux-x64)' },
+ { key: 'osx-x64', label: 'macOS (osx-x64)' },
+ ];
+ const rows = [];
+ for (const platform of platforms) {
+ const artifact = artifacts.find(a => a.name.includes(platform.key));
+ if (!artifact) {
+ continue;
+ }
+ const url = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`;
+ rows.push(`| ${platform.label} | [\`${artifact.name}\`](${url}) |`);
+ }
+ if (rows.length === 0) {
+ core.info('No platform artifacts on this run; nothing to comment.');
+ return;
+ }
+
+ const marker = '';
+ const body = [
+ marker,
+ `### 📦 Build artifacts — \`${run.head_sha.substring(0, 7)}\``,
+ '',
+ '| Platform | Download |',
+ '| --- | --- |',
+ ...rows,
+ '',
+ `From [build run #${run.run_number}](${run.html_url}). ` +
+ 'Downloads require a GitHub login and expire after 90 days.',
+ ].join('\n');
+
+ // Upsert one comment so repeated builds refresh it in place.
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner, repo, issue_number: prNumber, per_page: 100,
+ });
+ const existing = comments.find(c => c.body && c.body.includes(marker));
+ if (existing) {
+ await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
+ } else {
+ await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
+ }
diff --git a/SharpEmu.slnx b/SharpEmu.slnx
index 045ee20..8a59a9a 100644
--- a/SharpEmu.slnx
+++ b/SharpEmu.slnx
@@ -11,6 +11,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json
index fb58102..6985b08 100644
--- a/src/SharpEmu.CLI/packages.lock.json
+++ b/src/SharpEmu.CLI/packages.lock.json
@@ -216,9 +216,9 @@
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
- "SharpEmu.HLE": "[1.0.0, )",
- "SharpEmu.Libs": "[1.0.0, )",
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.HLE": "[0.0.1, )",
+ "SharpEmu.Libs": "[0.0.1, )",
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.gui": {
@@ -228,20 +228,22 @@
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
- "SharpEmu.Logging": "[1.0.0, )",
+ "SharpEmu.Logging": "[0.0.1, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
- "SharpEmu.HLE": "[1.0.0, )",
+ "SharpEmu.HLE": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler.Vulkan": "[0.0.1, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
@@ -252,6 +254,18 @@
"sharpemu.logging": {
"type": "Project"
},
+ "sharpemu.shadercompiler": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.HLE": "[0.0.1, )"
+ }
+ },
+ "sharpemu.shadercompiler.vulkan": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.ShaderCompiler": "[0.0.1, )"
+ }
+ },
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
diff --git a/src/SharpEmu.Core/packages.lock.json b/src/SharpEmu.Core/packages.lock.json
index fe059ca..eb7d671 100644
--- a/src/SharpEmu.Core/packages.lock.json
+++ b/src/SharpEmu.Core/packages.lock.json
@@ -84,13 +84,15 @@
"sharpemu.hle": {
"type": "Project",
"dependencies": {
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
- "SharpEmu.HLE": "[1.0.0, )",
+ "SharpEmu.HLE": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler.Vulkan": "[0.0.1, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
@@ -101,6 +103,18 @@
"sharpemu.logging": {
"type": "Project"
},
+ "sharpemu.shadercompiler": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.HLE": "[0.0.1, )"
+ }
+ },
+ "sharpemu.shadercompiler.vulkan": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.ShaderCompiler": "[0.0.1, )"
+ }
+ },
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index 7e10146..84bc75e 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -1,11 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+using System.Collections.Concurrent;
using SharpEmu.HLE;
+using SharpEmu.Libs.Gpu;
+using SharpEmu.ShaderCompiler;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
-using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace SharpEmu.Libs.Agc;
@@ -157,13 +159,14 @@ public static class AgcExports
private static readonly HashSet _tracedComputeShaders = new();
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
private static readonly HashSet _tracedSubmittedDrawOpcodes = new();
- private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
- private static readonly Dictionary<
- (ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
- (byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
- private static readonly Dictionary<
+ // Concurrent so the per-draw/per-dispatch hit path is lock-free (and no longer
+ // shares _submitTraceGate with tracing).
+ private static readonly ConcurrentDictionary<
+ (ulong Es, ulong EsState, ulong Ps, ulong PsState, ulong OutputLayout, uint OutputCount, uint Attributes),
+ (IGuestCompiledShader Vertex, IGuestCompiledShader Pixel)> _graphicsShaderCache = new();
+ private static readonly ConcurrentDictionary<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
- byte[]> _computeSpirvCache = new();
+ IGuestCompiledShader> _computeShaderCache = new();
private static readonly Dictionary _shaderHeadersByCode = new();
private static readonly bool _traceAgc = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
@@ -334,17 +337,20 @@ public static class AgcExports
ulong ExportShaderAddress,
ulong PixelShaderAddress,
uint PrimitiveType,
- byte[] VertexSpirv,
- byte[] PixelSpirv,
+ IGuestCompiledShader VertexShader,
+ IGuestCompiledShader PixelShader,
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
- VulkanGuestIndexBuffer? IndexBuffer,
+ GuestIndexBuffer? IndexBuffer,
IReadOnlyList Textures,
IReadOnlyList GlobalMemoryBindings,
IReadOnlyList VertexInputs,
IReadOnlyList RenderTargets,
- VulkanGuestRenderState RenderState);
+ // The seam-shaped view of RenderTargets, built once here so the per-frame
+ // submit path does not rebuild it for every draw of a cached translation.
+ IReadOnlyList GuestTargets,
+ GuestRenderState RenderState);
private sealed record TranslatedImageBinding(
TextureDescriptor Descriptor,
@@ -2942,7 +2948,7 @@ public static class AgcExports
handle,
displayBufferIndex,
out var cachedDisplayBuffer) &&
- VulkanVideoPresenter.TrySubmitGuestImage(
+ GuestGpu.Current.TrySubmitGuestImage(
cachedDisplayBuffer.Address,
cachedDisplayBuffer.Width,
cachedDisplayBuffer.Height,
@@ -2966,11 +2972,11 @@ public static class AgcExports
displayBufferIndex,
translatedDisplayBuffer,
"draw-fallback");
- var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
+ var textures = CreateGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
var globalMemoryBuffers =
- CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
- VulkanVideoPresenter.SubmitTranslatedDraw(
- translatedDraw.PixelSpirv,
+ CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
+ GuestGpu.Current.SubmitTranslatedDraw(
+ translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDisplayBuffer.Width,
@@ -2978,7 +2984,7 @@ public static class AgcExports
translatedDraw.AttributeCount);
TraceAgcShader(
$"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " +
- $"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " +
+ $"spirv={translatedDraw.PixelShader.Payload.Length} textures={textures.Count} " +
$"global_buffers={globalMemoryBuffers.Count} " +
$"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}");
@@ -3013,7 +3019,7 @@ public static class AgcExports
displayBufferIndex,
out var displayBuffer))
{
- VulkanVideoPresenter.SubmitGuestDraw(
+ GuestGpu.Current.SubmitGuestDraw(
state.GuestDrawKind,
displayBuffer.Width,
displayBuffer.Height);
@@ -3409,29 +3415,23 @@ public static class AgcExports
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
- var textures = CreateVulkanGuestDrawTextures(
+ var textures = CreateGuestDrawTextures(
ctx,
translatedDraw.Textures,
out _);
var globalMemoryBuffers =
- CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
+ CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
var vertexBuffers =
- CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs);
+ CreateGuestVertexBuffers(translatedDraw.VertexInputs);
TraceRectListVertices(translatedDraw, vertexBuffers);
TraceGrassDrawVertices(translatedDraw, textures, vertexBuffers);
- VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
- translatedDraw.PixelSpirv,
+ GuestGpu.Current.SubmitOffscreenTranslatedDraw(
+ translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
- translatedDraw.RenderTargets.Select(target =>
- new VulkanGuestRenderTarget(
- target.Address,
- target.Width,
- target.Height,
- target.Format,
- target.NumberType)).ToArray(),
- translatedDraw.VertexSpirv,
+ translatedDraw.GuestTargets,
+ translatedDraw.VertexShader,
translatedDraw.VertexCount,
translatedDraw.InstanceCount,
translatedDraw.PrimitiveType,
@@ -3445,14 +3445,14 @@ public static class AgcExports
.FirstOrDefault(binding => binding.IsStorage);
if (storageTarget is not null)
{
- var textures = CreateVulkanGuestDrawTextures(
+ var textures = CreateGuestDrawTextures(
ctx,
translatedDraw.Textures,
out _);
var globalMemoryBuffers =
- CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
- VulkanVideoPresenter.SubmitStorageTranslatedDraw(
- translatedDraw.PixelSpirv,
+ CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
+ GuestGpu.Current.SubmitStorageTranslatedDraw(
+ translatedDraw.PixelShader,
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
@@ -3561,14 +3561,14 @@ public static class AgcExports
.Where(target => HasPixelColorExport(pixelState, target.Slot))
.OrderBy(target => target.Slot)
.ToArray();
- var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
+ var renderTargetOutputKinds = new Gen5PixelOutputKind[renderTargets.Length];
for (var index = 0; index < renderTargets.Length; index++)
{
var target = renderTargets[index];
- if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
+ if (!GuestGpu.Current.TryGetRenderTargetOutputKind(
target.Format,
target.NumberType,
- out renderTargetFormats[index]))
+ out renderTargetOutputKinds[index]))
{
error =
$"unsupported color target format={target.Format} number_type={target.NumberType}";
@@ -3576,16 +3576,17 @@ public static class AgcExports
}
}
- var pixelOutputs = renderTargets
- .Select((target, location) => new Gen5PixelOutputBinding(
- target.Slot,
- (uint)location,
- renderTargetFormats[location].OutputKind))
- .ToArray();
- var outputLayout = string.Join(
- ';',
- pixelOutputs.Select(output =>
- $"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
+ // Exact packed encoding of the output layout — guest slot (6 bits, CB targets are
+ // 0-7) plus output kind (2 bits) per target, host locations being the sequential
+ // byte positions. Replaces a per-draw LINQ + string build that allocated on every
+ // draw, cache hit or not; the target count disambiguates trailing zero bytes.
+ var outputLayout = 0UL;
+ for (var index = 0; index < renderTargets.Length; index++)
+ {
+ outputLayout |= (ulong)(((renderTargets[index].Slot & 0x3Fu) << 2) |
+ (uint)renderTargetOutputKinds[index]) << (index * 8);
+ }
+
var attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
@@ -3595,19 +3596,25 @@ public static class AgcExports
pixelShaderAddress,
pixelStateFingerprint,
outputLayout,
+ (uint)renderTargets.Length,
attributeCount);
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
- (byte[] Vertex, byte[] Pixel) compiled;
- lock (_submitTraceGate)
- {
- _graphicsSpirvCache.TryGetValue(shaderKey, out compiled);
- }
+ _graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
if (compiled.Vertex is null || compiled.Pixel is null)
{
- if (!Gen5SpirvTranslator.TryCompilePixelShader(
+ var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
+ for (var location = 0; location < renderTargets.Length; location++)
+ {
+ pixelOutputs[location] = new Gen5PixelOutputBinding(
+ renderTargets[location].Slot,
+ (uint)location,
+ renderTargetOutputKinds[location]);
+ }
+
+ if (!GuestGpu.Current.TryCompilePixelShader(
pixelState,
pixelEvaluation,
pixelOutputs,
@@ -3617,7 +3624,7 @@ public static class AgcExports
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: 0,
scalarRegisterBufferIndex: totalGlobalBuffers) ||
- !Gen5SpirvTranslator.TryCompileVertexShader(
+ !GuestGpu.Current.TryCompileVertexShader(
exportState,
exportEvaluation,
out var vertexShader,
@@ -3630,23 +3637,20 @@ public static class AgcExports
return false;
}
- compiled = (vertexShader.Spirv, pixelShader.Spirv);
- DumpSpirv(
+ compiled = (vertexShader!, pixelShader!);
+ DumpCompiledShader(
"vs",
exportShaderAddress,
exportStateFingerprint,
compiled.Vertex,
exportState.Program);
- DumpSpirv(
+ DumpCompiledShader(
"ps",
pixelShaderAddress,
pixelStateFingerprint,
compiled.Pixel,
pixelState.Program);
- lock (_submitTraceGate)
- {
- _graphicsSpirvCache.TryAdd(shaderKey, compiled);
- }
+ _graphicsShaderCache.TryAdd(shaderKey, compiled);
}
var imageBindings = pixelEvaluation.ImageBindings
@@ -3683,6 +3687,17 @@ public static class AgcExports
IReadOnlyList vertexInputs =
exportEvaluation.VertexInputs ?? [];
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
+ var guestTargets = new GuestRenderTarget[renderTargets.Length];
+ for (var index = 0; index < renderTargets.Length; index++)
+ {
+ guestTargets[index] = new GuestRenderTarget(
+ renderTargets[index].Address,
+ renderTargets[index].Width,
+ renderTargets[index].Height,
+ renderTargets[index].Format,
+ renderTargets[index].NumberType);
+ }
+
draw = new TranslatedGuestDraw(
exportShaderAddress,
pixelShaderAddress,
@@ -3692,11 +3707,12 @@ public static class AgcExports
attributeCount,
vertexCount,
state.InstanceCount,
- indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
+ indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
textures,
globalMemoryBindings,
vertexInputs,
renderTargets,
+ guestTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
textures,
@@ -3717,8 +3733,8 @@ public static class AgcExports
/// Treat precisely that draw shape as an overwrite only when every MRT
/// attachment uses the same premultiplied blend pattern.
///
- private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
- VulkanGuestRenderState renderState,
+ private static GuestRenderState ApplyTransparentPremultipliedFillClear(
+ GuestRenderState renderState,
IReadOnlyList textures,
IReadOnlyList vertexInputs,
IReadOnlyList pixelUserData)
@@ -3748,7 +3764,7 @@ public static class AgcExports
};
}
- private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
+ private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
blend is
{
Enable: true,
@@ -3757,7 +3773,7 @@ public static class AgcExports
ColorFunc: 0,
};
- private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
+ private static GuestIndexBuffer? CreateGuestIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
uint indexCount)
@@ -3775,7 +3791,7 @@ public static class AgcExports
var address = state.IndexBufferAddress + byteOffset;
return (ctx.Memory.TryRead(address, data) ||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data))
- ? new VulkanGuestIndexBuffer(data, is32Bit)
+ ? new GuestIndexBuffer(data, is32Bit)
: null;
}
@@ -3943,19 +3959,19 @@ public static class AgcExports
return targets;
}
- private static VulkanGuestRenderState CreateRenderState(
+ private static GuestRenderState CreateRenderState(
IReadOnlyDictionary registers,
IReadOnlyList targets,
Gen5ShaderState pixelState)
{
if (targets.Count == 0)
{
- return VulkanGuestRenderState.Default;
+ return GuestRenderState.Default;
}
var target = targets[0];
var scissor = DecodeScissor(registers, target.Width, target.Height);
- return new VulkanGuestRenderState(
+ return new GuestRenderState(
targets.Select(target =>
{
var blend = DecodeBlendState(registers, target.Slot);
@@ -3968,7 +3984,7 @@ public static class AgcExports
DecodeViewport(registers, target.Width, target.Height, scissor));
}
- private static VulkanGuestBlendState DecodeBlendState(
+ private static GuestBlendState DecodeBlendState(
IReadOnlyDictionary registers,
uint slot)
{
@@ -3979,7 +3995,7 @@ public static class AgcExports
}
registers.TryGetValue(CbBlend0Control + slot, out var control);
- return new VulkanGuestBlendState(
+ return new GuestBlendState(
((control >> 30) & 1u) != 0,
control & 0x1Fu,
(control >> 8) & 0x1Fu,
@@ -3991,14 +4007,14 @@ public static class AgcExports
writeMask);
}
- private static VulkanGuestRect? DecodeScissor(
+ private static GuestRect? DecodeScissor(
IReadOnlyDictionary registers,
uint targetWidth,
uint targetHeight)
{
if (targetWidth == 0 || targetHeight == 0)
{
- return new VulkanGuestRect(0, 0, 0, 0);
+ return new GuestRect(0, 0, 0, 0);
}
var left = 0;
@@ -4063,22 +4079,22 @@ public static class AgcExports
return null;
}
- return new VulkanGuestRect(
+ return new GuestRect(
left,
top,
checked((uint)(right - left)),
checked((uint)(bottom - top)));
}
- private static VulkanGuestViewport? DecodeViewport(
+ private static GuestViewport? DecodeViewport(
IReadOnlyDictionary registers,
uint targetWidth,
uint targetHeight,
- VulkanGuestRect? scissor)
+ GuestRect? scissor)
{
if (targetWidth == 0 || targetHeight == 0)
{
- return new VulkanGuestViewport(0, 0, 0, 0, 0, 1);
+ return new GuestViewport(0, 0, 0, 0, 0, 1);
}
var minDepth = 0f;
@@ -4104,7 +4120,7 @@ public static class AgcExports
xScale > 0f &&
yScale != 0f)
{
- return new VulkanGuestViewport(
+ return new GuestViewport(
xOffset - xScale,
yOffset - yScale,
xScale * 2f,
@@ -4117,10 +4133,10 @@ public static class AgcExports
{
return minDepth == 0f && maxDepth == 1f
? null
- : new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
+ : new GuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
}
- return new VulkanGuestViewport(
+ return new GuestViewport(
rect.X,
rect.Y,
rect.Width,
@@ -4297,7 +4313,7 @@ public static class AgcExports
var blend = draw.RenderState.Blend;
TraceAgcShader(
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
- $"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelSpirv.Length} " +
+ $"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelShader.Payload.Length} " +
$"primitive=0x{draw.PrimitiveType:X} " +
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
@@ -4307,16 +4323,16 @@ public static class AgcExports
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
}
- private static IReadOnlyList CreateVulkanGuestDrawTextures(
+ private static IReadOnlyList CreateGuestDrawTextures(
CpuContext ctx,
IReadOnlyList bindings,
out int fallbackTextureCount)
{
- var textures = new List(bindings.Count);
+ var textures = new List(bindings.Count);
fallbackTextureCount = 0;
foreach (var binding in bindings)
{
- if (TryCreateVulkanGuestDrawTexture(
+ if (TryCreateGuestDrawTexture(
ctx,
binding.Descriptor,
binding.IsStorage,
@@ -4335,13 +4351,13 @@ public static class AgcExports
return textures;
}
- private static IReadOnlyList CreateVulkanGuestMemoryBuffers(
+ private static IReadOnlyList CreateGuestMemoryBuffers(
IReadOnlyList bindings)
{
- var buffers = new VulkanGuestMemoryBuffer[bindings.Count];
+ var buffers = new GuestMemoryBuffer[bindings.Count];
for (var index = 0; index < bindings.Count; index++)
{
- buffers[index] = new VulkanGuestMemoryBuffer(
+ buffers[index] = new GuestMemoryBuffer(
bindings[index].BaseAddress,
bindings[index].Data);
}
@@ -4349,14 +4365,14 @@ public static class AgcExports
return buffers;
}
- private static IReadOnlyList CreateVulkanGuestVertexBuffers(
+ private static IReadOnlyList CreateGuestVertexBuffers(
IReadOnlyList bindings)
{
- var buffers = new VulkanGuestVertexBuffer[bindings.Count];
+ var buffers = new GuestVertexBuffer[bindings.Count];
for (var index = 0; index < bindings.Count; index++)
{
var binding = bindings[index];
- buffers[index] = new VulkanGuestVertexBuffer(
+ buffers[index] = new GuestVertexBuffer(
binding.Location,
binding.ComponentCount,
binding.DataFormat,
@@ -4370,13 +4386,13 @@ public static class AgcExports
return buffers;
}
- private static bool TryCreateVulkanGuestDrawTexture(
+ private static bool TryCreateGuestDrawTexture(
CpuContext ctx,
TextureDescriptor descriptor,
bool isStorage,
uint mipLevel,
IReadOnlyList samplerDescriptor,
- out VulkanGuestDrawTexture texture)
+ out GuestDrawTexture texture)
{
texture = default!;
if (descriptor.Type != Gen5TextureType2D ||
@@ -4412,12 +4428,12 @@ public static class AgcExports
if (!isStorage &&
descriptor.Address != 0 &&
- VulkanVideoPresenter.IsGpuGuestImageAvailable(
+ GuestGpu.Current.IsGpuGuestImageAvailable(
descriptor.Address,
descriptor.Format,
descriptor.NumberType))
{
- texture = new VulkanGuestDrawTexture(
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4431,7 +4447,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
- Sampler: ToVulkanSampler(samplerDescriptor));
+ Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4451,7 +4467,7 @@ public static class AgcExports
}
}
- texture = new VulkanGuestDrawTexture(
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4465,7 +4481,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
- Sampler: ToVulkanSampler(samplerDescriptor));
+ Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4506,7 +4522,7 @@ public static class AgcExports
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
var rgba = source;
- texture = new VulkanGuestDrawTexture(
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -4520,7 +4536,7 @@ public static class AgcExports
Pitch: sourceWidth,
TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect,
- Sampler: ToVulkanSampler(samplerDescriptor));
+ Sampler: ToGuestSampler(samplerDescriptor));
return true;
}
@@ -4553,8 +4569,8 @@ public static class AgcExports
private static void TraceGrassDrawVertices(
TranslatedGuestDraw draw,
- IReadOnlyList textures,
- IReadOnlyList vertexBuffers)
+ IReadOnlyList textures,
+ IReadOnlyList vertexBuffers)
{
if (_grassTraceCount >= 6 ||
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
@@ -4593,7 +4609,7 @@ public static class AgcExports
private static void TraceRectListVertices(
TranslatedGuestDraw draw,
- IReadOnlyList vertexBuffers)
+ IReadOnlyList vertexBuffers)
{
if (draw.PrimitiveType != 0x11 ||
draw.IndexBuffer is not null ||
@@ -4676,7 +4692,7 @@ public static class AgcExports
}
}
- private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(
+ private static GuestDrawTexture CreateFallbackGuestDrawTexture(
bool isStorage,
uint format,
uint numberType)
@@ -4724,9 +4740,9 @@ public static class AgcExports
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
}
- private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList descriptor) =>
+ private static GuestSampler ToGuestSampler(IReadOnlyList descriptor) =>
descriptor.Count >= 4
- ? new VulkanGuestSampler(
+ ? new GuestSampler(
descriptor[0],
descriptor[1],
descriptor[2],
@@ -4924,47 +4940,39 @@ public static class AgcExports
localSizeX,
localSizeY,
localSizeZ);
- byte[] computeSpirv;
- lock (_submitTraceGate)
- {
- _computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!);
- }
+ _computeShaderCache.TryGetValue(shaderKey, out var computeShader);
- if (computeSpirv is null &&
- Gen5SpirvTranslator.TryCompileComputeShader(
+ if (computeShader is null &&
+ GuestGpu.Current.TryCompileComputeShader(
shaderState,
evaluation,
localSizeX,
localSizeY,
localSizeZ,
- out var compiledCompute,
+ out computeShader,
out computeError))
{
- computeSpirv = compiledCompute.Spirv;
- DumpSpirv(
+ DumpCompiledShader(
"cs",
shaderAddress,
shaderKey.Item2,
- computeSpirv,
+ computeShader!,
shaderState.Program);
}
- if (computeSpirv is not null)
+ if (computeShader is not null)
{
- lock (_submitTraceGate)
- {
- _computeSpirvCache.TryAdd(shaderKey, computeSpirv);
- }
+ _computeShaderCache.TryAdd(shaderKey, computeShader);
- var textures = CreateVulkanGuestDrawTextures(
+ var textures = CreateGuestDrawTextures(
ctx,
translatedBindings,
out _);
var globalMemoryBuffers =
- CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
- VulkanVideoPresenter.SubmitComputeDispatch(
+ CreateGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
+ GuestGpu.Current.SubmitComputeDispatch(
shaderAddress,
- computeSpirv,
+ computeShader,
textures,
globalMemoryBuffers,
dispatch.GroupCountX,
@@ -5135,7 +5143,7 @@ public static class AgcExports
}
}
else if (source is { } cachedSourceTexture &&
- VulkanVideoPresenter.TrySubmitGuestImageBlit(
+ GuestGpu.Current.TrySubmitGuestImageBlit(
cachedSourceTexture.Address,
cachedSourceTexture.Width,
cachedSourceTexture.Height,
@@ -5489,7 +5497,7 @@ public static class AgcExports
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
}
- if (Gen5SpirvTranslator.TryCompilePixelShader(
+ if (GuestGpu.Current.TryCompilePixelShader(
pixelState,
evaluation,
[new(0, 0, Gen5PixelOutputKind.Float)],
@@ -5498,7 +5506,7 @@ public static class AgcExports
{
TraceAgcShader(
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
- $"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " +
+ $"bytes={compiledPixel!.Payload.Length} bindings={evaluation.ImageBindings.Count} " +
$"global_buffers={evaluation.GlobalMemoryBindings.Count}");
}
else
@@ -6388,14 +6396,14 @@ public static class AgcExports
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
- private static void DumpSpirv(
+ private static void DumpCompiledShader(
string stage,
ulong shaderAddress,
ulong stateFingerprint,
- byte[] spirv,
+ IGuestCompiledShader shader,
Gen5ShaderProgram program)
{
- if (spirv.Length == 0 ||
+ if (shader.Payload.Length == 0 ||
!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"),
"1",
@@ -6407,7 +6415,9 @@ public static class AgcExports
var directory = Path.Combine(AppContext.BaseDirectory, "shader-dumps");
Directory.CreateDirectory(directory);
var name = $"{shaderAddress:X16}-{stateFingerprint:X16}.{stage}";
- File.WriteAllBytes(Path.Combine(directory, $"{name}.spv"), spirv);
+ File.WriteAllBytes(
+ Path.Combine(directory, $"{name}.{shader.PayloadFileExtension}"),
+ shader.Payload);
var lines = new List(program.Instructions.Count + 2)
{
diff --git a/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs b/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs
new file mode 100644
index 0000000..aa824cc
--- /dev/null
+++ b/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs
@@ -0,0 +1,30 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using SharpEmu.Libs.Kernel;
+using SharpEmu.ShaderCompiler;
+
+namespace SharpEmu.Libs.Agc;
+
+///
+/// Wires the backend-neutral shader compiler to this assembly's HLE services. The
+/// module initializer runs before any Libs code can invoke the evaluator, so the hook
+/// is always installed first.
+///
+internal static class AgcShaderCompilerHooks
+{
+ [ModuleInitializer]
+ [SuppressMessage(
+ "Usage",
+ "CA2255:The 'ModuleInitializer' attribute should not be used in libraries",
+ Justification = "This is the rule's intended advanced scenario: the hook must be " +
+ "installed before any code path can reach the evaluator, and every such path " +
+ "enters through this assembly.")]
+ internal static void Install()
+ {
+ Gen5ShaderScalarEvaluator.FallbackMemoryReader =
+ KernelMemoryCompatExports.TryReadTrackedLibcHeap;
+ }
+}
diff --git a/src/SharpEmu.Libs/Gpu/GuestGpu.cs b/src/SharpEmu.Libs/Gpu/GuestGpu.cs
new file mode 100644
index 0000000..78c6b1f
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/GuestGpu.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.Libs.Gpu.Vulkan;
+
+namespace SharpEmu.Libs.Gpu;
+
+///
+/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
+/// host seam: static HLE export classes resolve the renderer through .
+/// Vulkan is the only backend today; Metal/DX12 slot in here.
+///
+internal static class GuestGpu
+{
+ private static readonly Lazy Instance = new(static () => new VulkanGuestGpuBackend());
+
+ public static IGuestGpuBackend Current => Instance.Value;
+}
diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs
new file mode 100644
index 0000000..759b40d
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs
@@ -0,0 +1,116 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.Libs.Gpu;
+
+// The types that cross the guest-GPU backend seam. Every field is either a neutral
+// primitive (dimensions, counts, host pixel bytes) or a raw guest/AGC value (guest
+// addresses, guest format and number-type codes, guest register bitfields). Host
+// graphics-API values must never appear here: each backend owns the guest -> native
+// translation for its API.
+
+/// A guest texture referenced by a draw or dispatch. Format/NumberType/
+/// TileMode/DstSelect are raw guest descriptor codes.
+internal sealed record GuestDrawTexture(
+ ulong Address,
+ uint Width,
+ uint Height,
+ uint Format,
+ uint NumberType,
+ byte[] RgbaPixels,
+ bool IsFallback,
+ bool IsStorage,
+ uint MipLevels = 1,
+ uint MipLevel = 0,
+ uint Pitch = 0,
+ uint TileMode = 0,
+ uint DstSelect = 0xFAC,
+ GuestSampler Sampler = default);
+
+/// Raw guest sampler descriptor dwords, copied verbatim from guest memory.
+internal readonly record struct GuestSampler(
+ uint Word0,
+ uint Word1,
+ uint Word2,
+ uint Word3);
+
+internal sealed record GuestMemoryBuffer(
+ ulong BaseAddress,
+ byte[] Data);
+
+/// DataFormat/NumberFormat are raw guest vertex-attribute codes.
+internal sealed record GuestVertexBuffer(
+ uint Location,
+ uint ComponentCount,
+ uint DataFormat,
+ uint NumberFormat,
+ ulong BaseAddress,
+ uint Stride,
+ uint OffsetBytes,
+ byte[] Data);
+
+internal sealed record GuestIndexBuffer(
+ byte[] Data,
+ bool Is32Bit);
+
+internal readonly record struct GuestRect(
+ int X,
+ int Y,
+ uint Width,
+ uint Height);
+
+internal readonly record struct GuestViewport(
+ float X,
+ float Y,
+ float Width,
+ float Height,
+ float MinDepth,
+ float MaxDepth);
+
+/// Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
+/// defaults (1/0) are the guest ONE/ZERO codes.
+internal readonly record struct GuestBlendState(
+ bool Enable,
+ uint ColorSrcFactor,
+ uint ColorDstFactor,
+ uint ColorFunc,
+ uint AlphaSrcFactor,
+ uint AlphaDstFactor,
+ uint AlphaFunc,
+ bool SeparateAlphaBlend,
+ uint WriteMask)
+{
+ public static GuestBlendState Default { get; } = new(
+ Enable: false,
+ ColorSrcFactor: 1,
+ ColorDstFactor: 0,
+ ColorFunc: 0,
+ AlphaSrcFactor: 1,
+ AlphaDstFactor: 0,
+ AlphaFunc: 0,
+ SeparateAlphaBlend: false,
+ WriteMask: 0xFu);
+}
+
+internal sealed record GuestRenderState(
+ IReadOnlyList Blends,
+ GuestRect? Scissor,
+ GuestViewport? Viewport)
+{
+ public static GuestRenderState Default { get; } = new(
+ [GuestBlendState.Default],
+ Scissor: null,
+ Viewport: null);
+
+ public GuestBlendState Blend =>
+ Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
+}
+
+/// Format/NumberType are raw guest render-target register codes.
+internal sealed record GuestRenderTarget(
+ ulong Address,
+ uint Width,
+ uint Height,
+ uint Format,
+ uint NumberType,
+ uint MipLevels = 1);
diff --git a/src/SharpEmu.Libs/Gpu/IGuestCompiledShader.cs b/src/SharpEmu.Libs/Gpu/IGuestCompiledShader.cs
new file mode 100644
index 0000000..995df03
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/IGuestCompiledShader.cs
@@ -0,0 +1,19 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.Libs.Gpu;
+
+///
+/// A guest shader compiled by a backend, opaque to the export layers: only the backend
+/// that produced it can submit it. is the backend-defined compiled
+/// bytes (SPIR-V words for Vulkan; MSL/DXIL for future backends), exposed solely for
+/// diagnostics dumps and size traces — callers must never interpret it.
+///
+internal interface IGuestCompiledShader
+{
+ byte[] Payload { get; }
+
+ /// File extension for diagnostics dumps of ("spv",
+ /// "msl", ...), so dumps stay honestly labeled whatever the backend.
+ string PayloadFileExtension { get; }
+}
diff --git a/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs
new file mode 100644
index 0000000..0963fc2
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs
@@ -0,0 +1,140 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.ShaderCompiler;
+
+namespace SharpEmu.Libs.Gpu;
+
+///
+/// The guest-GPU backend seam: everything the AGC/VideoOut export layers need from a
+/// host renderer, expressed in guest-domain terms so Vulkan, Metal, and DX12 backends
+/// can each translate to their native API. Two rules keep it that way: no host-API
+/// value (formats, blend enums, barrier or pass concepts) may cross this interface,
+/// and submission is coarse-grained — synchronization is a backend-internal concern.
+///
+/// Shader compilation also lives behind the seam: the backend owns its codegen and
+/// returns opaque handles that only it can submit.
+///
+internal interface IGuestGpuBackend
+{
+ /// Starts the presenter (window + device) once; safe to call repeatedly.
+ void EnsureStarted(uint width, uint height);
+
+ // Shader compilation. The optional base/index parameters describe how a multi-stage
+ // draw lays both stages' resources into one flat per-role slot space (buffers,
+ // images, scalar-spill slots); each backend maps those slots to its own API binding
+ // model. -1 keeps the emitter's single-stage defaults.
+
+ bool TryCompileVertexShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ out IGuestCompiledShader? shader,
+ out string error,
+ int globalBufferBase = 0,
+ int totalGlobalBufferCount = -1,
+ int imageBindingBase = 0,
+ int scalarRegisterBufferIndex = -1);
+
+ bool TryCompilePixelShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ IReadOnlyList outputs,
+ out IGuestCompiledShader? shader,
+ out string error,
+ int globalBufferBase = 0,
+ int totalGlobalBufferCount = -1,
+ int imageBindingBase = 0,
+ int scalarRegisterBufferIndex = -1);
+
+ bool TryCompileComputeShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ uint localSizeX,
+ uint localSizeY,
+ uint localSizeZ,
+ out IGuestCompiledShader? shader,
+ out string error);
+
+ void HideSplashScreen();
+
+ /// Presents one CPU-produced BGRA frame.
+ void Submit(byte[] bgraFrame, uint width, uint height);
+
+ /// Presents a recognized fixed-function guest draw (see GuestDrawKind).
+ void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height);
+
+ void SubmitTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint width,
+ uint height,
+ uint attributeCount,
+ IGuestCompiledShader? vertexShader = null,
+ uint vertexCount = 3,
+ uint instanceCount = 1,
+ uint primitiveType = 4,
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null);
+
+ void SubmitOffscreenTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint attributeCount,
+ IReadOnlyList targets,
+ IGuestCompiledShader? vertexShader = null,
+ uint vertexCount = 3,
+ uint instanceCount = 1,
+ uint primitiveType = 4,
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null);
+
+ void SubmitStorageTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint attributeCount,
+ uint width,
+ uint height);
+
+ void SubmitComputeDispatch(
+ ulong shaderAddress,
+ IGuestCompiledShader computeShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint groupCountX,
+ uint groupCountY,
+ uint groupCountZ);
+
+ bool TrySubmitGuestImage(
+ ulong address,
+ uint width,
+ uint height,
+ uint pitchInPixel);
+
+ /// Registers a display buffer with its guest texture format tag.
+ void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
+
+ /// Format/numberType are raw guest texture descriptor codes.
+ bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType);
+
+ bool TrySubmitGuestImageBlit(
+ ulong sourceAddress,
+ uint sourceWidth,
+ uint sourceHeight,
+ uint sourceFormat,
+ ulong destinationAddress,
+ uint destinationWidth,
+ uint destinationHeight,
+ uint destinationFormat);
+
+ ///
+ /// Whether the backend supports the guest render-target format, and how its pixel
+ /// outputs are typed. Deliberately does not expose the backend's native format —
+ /// the guest codes cross the seam and each backend maps them internally.
+ ///
+ bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
+}
diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanCompiledGuestShader.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanCompiledGuestShader.cs
new file mode 100644
index 0000000..ebb1d46
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanCompiledGuestShader.cs
@@ -0,0 +1,12 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.Libs.Gpu.Vulkan;
+
+/// The Vulkan backend's compiled shader: raw SPIR-V words.
+internal sealed record VulkanCompiledGuestShader(byte[] Spirv) : IGuestCompiledShader
+{
+ public byte[] Payload => Spirv;
+
+ public string PayloadFileExtension => "spv";
+}
diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs
new file mode 100644
index 0000000..702d6de
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs
@@ -0,0 +1,251 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.Libs.VideoOut;
+using SharpEmu.ShaderCompiler;
+using SharpEmu.ShaderCompiler.Vulkan;
+
+namespace SharpEmu.Libs.Gpu.Vulkan;
+
+///
+/// Vulkan backend for the guest-GPU seam: SPIR-V codegen via
+/// SharpEmu.ShaderCompiler.Vulkan, rendering via a thin adapter over the existing
+/// VulkanVideoPresenter statics (folding the presenter into an instance type is
+/// follow-up work, not a seam concern).
+///
+internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
+{
+ public bool TryCompileVertexShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ out IGuestCompiledShader? shader,
+ out string error,
+ int globalBufferBase = 0,
+ int totalGlobalBufferCount = -1,
+ int imageBindingBase = 0,
+ int scalarRegisterBufferIndex = -1)
+ {
+ shader = null;
+ if (!Gen5SpirvTranslator.TryCompileVertexShader(
+ state,
+ evaluation,
+ out var compiled,
+ out error,
+ globalBufferBase,
+ totalGlobalBufferCount,
+ imageBindingBase,
+ scalarRegisterBufferIndex))
+ {
+ return false;
+ }
+
+ shader = new VulkanCompiledGuestShader(compiled.Spirv);
+ return true;
+ }
+
+ public bool TryCompilePixelShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ IReadOnlyList outputs,
+ out IGuestCompiledShader? shader,
+ out string error,
+ int globalBufferBase = 0,
+ int totalGlobalBufferCount = -1,
+ int imageBindingBase = 0,
+ int scalarRegisterBufferIndex = -1)
+ {
+ shader = null;
+ if (!Gen5SpirvTranslator.TryCompilePixelShader(
+ state,
+ evaluation,
+ outputs,
+ out var compiled,
+ out error,
+ globalBufferBase,
+ totalGlobalBufferCount,
+ imageBindingBase,
+ scalarRegisterBufferIndex))
+ {
+ return false;
+ }
+
+ shader = new VulkanCompiledGuestShader(compiled.Spirv);
+ return true;
+ }
+
+ public bool TryCompileComputeShader(
+ Gen5ShaderState state,
+ Gen5ShaderEvaluation evaluation,
+ uint localSizeX,
+ uint localSizeY,
+ uint localSizeZ,
+ out IGuestCompiledShader? shader,
+ out string error)
+ {
+ shader = null;
+ if (!Gen5SpirvTranslator.TryCompileComputeShader(
+ state,
+ evaluation,
+ localSizeX,
+ localSizeY,
+ localSizeZ,
+ out var compiled,
+ out error))
+ {
+ return false;
+ }
+
+ shader = new VulkanCompiledGuestShader(compiled.Spirv);
+ return true;
+ }
+
+ public void EnsureStarted(uint width, uint height) =>
+ VulkanVideoPresenter.EnsureStarted(width, height);
+
+ public void HideSplashScreen() =>
+ VulkanVideoPresenter.HideSplashScreen();
+
+ public void Submit(byte[] bgraFrame, uint width, uint height) =>
+ VulkanVideoPresenter.Submit(bgraFrame, width, height);
+
+ public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
+ VulkanVideoPresenter.SubmitGuestDraw(drawKind, width, height);
+
+ public void SubmitTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint width,
+ uint height,
+ uint attributeCount,
+ IGuestCompiledShader? vertexShader = null,
+ uint vertexCount = 3,
+ uint instanceCount = 1,
+ uint primitiveType = 4,
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null) =>
+ VulkanVideoPresenter.SubmitTranslatedDraw(
+ Spirv(pixelShader),
+ textures,
+ globalMemoryBuffers,
+ width,
+ height,
+ attributeCount,
+ vertexShader is null ? null : Spirv(vertexShader),
+ vertexCount,
+ instanceCount,
+ primitiveType,
+ indexBuffer,
+ vertexBuffers,
+ renderState);
+
+ public void SubmitOffscreenTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint attributeCount,
+ IReadOnlyList targets,
+ IGuestCompiledShader? vertexShader = null,
+ uint vertexCount = 3,
+ uint instanceCount = 1,
+ uint primitiveType = 4,
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null) =>
+ VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
+ Spirv(pixelShader),
+ textures,
+ globalMemoryBuffers,
+ attributeCount,
+ targets,
+ vertexShader is null ? null : Spirv(vertexShader),
+ vertexCount,
+ instanceCount,
+ primitiveType,
+ indexBuffer,
+ vertexBuffers,
+ renderState);
+
+ public void SubmitStorageTranslatedDraw(
+ IGuestCompiledShader pixelShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint attributeCount,
+ uint width,
+ uint height) =>
+ VulkanVideoPresenter.SubmitStorageTranslatedDraw(
+ Spirv(pixelShader),
+ textures,
+ globalMemoryBuffers,
+ attributeCount,
+ width,
+ height);
+
+ public void SubmitComputeDispatch(
+ ulong shaderAddress,
+ IGuestCompiledShader computeShader,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
+ uint groupCountX,
+ uint groupCountY,
+ uint groupCountZ) =>
+ VulkanVideoPresenter.SubmitComputeDispatch(
+ shaderAddress,
+ Spirv(computeShader),
+ textures,
+ globalMemoryBuffers,
+ groupCountX,
+ groupCountY,
+ groupCountZ);
+
+ public bool TrySubmitGuestImage(
+ ulong address,
+ uint width,
+ uint height,
+ uint pitchInPixel) =>
+ VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
+
+ public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
+ VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
+
+ public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
+ VulkanVideoPresenter.IsGpuGuestImageAvailable(address, format, numberType);
+
+ public bool TrySubmitGuestImageBlit(
+ ulong sourceAddress,
+ uint sourceWidth,
+ uint sourceHeight,
+ uint sourceFormat,
+ ulong destinationAddress,
+ uint destinationWidth,
+ uint destinationHeight,
+ uint destinationFormat) =>
+ VulkanVideoPresenter.TrySubmitGuestImageBlit(
+ sourceAddress,
+ sourceWidth,
+ sourceHeight,
+ sourceFormat,
+ destinationAddress,
+ destinationWidth,
+ destinationHeight,
+ destinationFormat);
+
+ public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
+ {
+ if (VulkanVideoPresenter.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
+ {
+ outputKind = format.OutputKind;
+ return true;
+ }
+
+ outputKind = default;
+ return false;
+ }
+
+ private static byte[] Spirv(IGuestCompiledShader shader) =>
+ shader is VulkanCompiledGuestShader vulkanShader
+ ? vulkanShader.Spirv
+ : throw new InvalidOperationException(
+ $"shader handle of type {shader.GetType().Name} was not compiled by the Vulkan backend");
+}
diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
index 70973f8..829622e 100644
--- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj
+++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
@@ -6,6 +6,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
diff --git a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs
index f5d632f..f172c48 100644
--- a/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs
+++ b/src/SharpEmu.Libs/SystemService/SystemServiceExports.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
+using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
@@ -93,7 +94,7 @@ public static class SystemServiceExports
LibraryName = "libSceSystemService")]
public static int SystemServiceHideSplashScreen(CpuContext ctx)
{
- VulkanVideoPresenter.HideSplashScreen();
+ GuestGpu.Current.HideSplashScreen();
return ctx.SetReturn(0);
}
diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
index dca2dd8..290a169 100644
--- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
+++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
+using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Audio;
using SharpEmu.Libs.Kernel;
using SharpEmu.Logging;
@@ -812,7 +813,7 @@ public static class VideoOutExports
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
}
- VulkanVideoPresenter.Submit(bgraFrame, width, height);
+ GuestGpu.Current.Submit(bgraFrame, width, height);
}
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
@@ -1203,7 +1204,7 @@ public static class VideoOutExports
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
guestImageAddress = displayBuffer.Address;
- guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
+ guestImageSubmitted = GuestGpu.Current.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
@@ -1334,14 +1335,14 @@ public static class VideoOutExports
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
}
- VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
+ GuestGpu.Current.EnsureStarted(attribute.Width, attribute.Height);
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
if (guestFormat != 0)
{
foreach (var address in addresses)
{
- VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
+ GuestGpu.Current.RegisterKnownDisplayBuffer(address, guestFormat);
}
}
@@ -1573,7 +1574,7 @@ public static class VideoOutExports
: 0u;
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
- // VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
+ // the backend keys its guest-image registry on (see VulkanVideoPresenter.
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
NormalizePixelFormat(pixelFormat) switch
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
index 1283705..a766825 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
@@ -7,6 +7,9 @@ using Silk.NET.Maths;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using Silk.NET.Input;
+using SharpEmu.Libs.Gpu;
+using SharpEmu.ShaderCompiler;
+using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using Silk.NET.Vulkan.Extensions.EXT;
@@ -22,111 +25,6 @@ using VkSemaphore = Silk.NET.Vulkan.Semaphore;
namespace SharpEmu.Libs.VideoOut;
-internal enum GuestDrawKind
-{
- None,
- FullscreenBarycentric,
-}
-
-internal sealed record VulkanGuestDrawTexture(
- ulong Address,
- uint Width,
- uint Height,
- uint Format,
- uint NumberType,
- byte[] RgbaPixels,
- bool IsFallback,
- bool IsStorage,
- uint MipLevels = 1,
- uint MipLevel = 0,
- uint Pitch = 0,
- uint TileMode = 0,
- uint DstSelect = 0xFAC,
- VulkanGuestSampler Sampler = default);
-
-internal readonly record struct VulkanGuestSampler(
- uint Word0,
- uint Word1,
- uint Word2,
- uint Word3);
-
-internal sealed record VulkanGuestMemoryBuffer(
- ulong BaseAddress,
- byte[] Data);
-
-internal sealed record VulkanGuestVertexBuffer(
- uint Location,
- uint ComponentCount,
- uint DataFormat,
- uint NumberFormat,
- ulong BaseAddress,
- uint Stride,
- uint OffsetBytes,
- byte[] Data);
-
-internal sealed record VulkanGuestIndexBuffer(
- byte[] Data,
- bool Is32Bit);
-
-internal readonly record struct VulkanGuestRect(
- int X,
- int Y,
- uint Width,
- uint Height);
-
-internal readonly record struct VulkanGuestViewport(
- float X,
- float Y,
- float Width,
- float Height,
- float MinDepth,
- float MaxDepth);
-
-internal readonly record struct VulkanGuestBlendState(
- bool Enable,
- uint ColorSrcFactor,
- uint ColorDstFactor,
- uint ColorFunc,
- uint AlphaSrcFactor,
- uint AlphaDstFactor,
- uint AlphaFunc,
- bool SeparateAlphaBlend,
- uint WriteMask)
-{
- public static VulkanGuestBlendState Default { get; } = new(
- Enable: false,
- ColorSrcFactor: 1,
- ColorDstFactor: 0,
- ColorFunc: 0,
- AlphaSrcFactor: 1,
- AlphaDstFactor: 0,
- AlphaFunc: 0,
- SeparateAlphaBlend: false,
- WriteMask: 0xFu);
-}
-
-internal sealed record VulkanGuestRenderState(
- IReadOnlyList Blends,
- VulkanGuestRect? Scissor,
- VulkanGuestViewport? Viewport)
-{
- public static VulkanGuestRenderState Default { get; } = new(
- [VulkanGuestBlendState.Default],
- Scissor: null,
- Viewport: null);
-
- public VulkanGuestBlendState Blend =>
- Blends.Count == 0 ? VulkanGuestBlendState.Default : Blends[0];
-}
-
-internal sealed record VulkanGuestRenderTarget(
- ulong Address,
- uint Width,
- uint Height,
- uint Format,
- uint NumberType,
- uint MipLevels = 1);
-
internal readonly record struct VulkanRenderTargetFormat(
Format Format,
Gen5PixelOutputKind OutputKind)
@@ -137,26 +35,26 @@ internal readonly record struct VulkanRenderTargetFormat(
internal sealed record VulkanTranslatedGuestDraw(
byte[] VertexSpirv,
byte[] PixelSpirv,
- IReadOnlyList Textures,
- IReadOnlyList GlobalMemoryBuffers,
- IReadOnlyList VertexBuffers,
+ IReadOnlyList Textures,
+ IReadOnlyList GlobalMemoryBuffers,
+ IReadOnlyList VertexBuffers,
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
uint PrimitiveType,
- VulkanGuestIndexBuffer? IndexBuffer,
- VulkanGuestRenderState RenderState);
+ GuestIndexBuffer? IndexBuffer,
+ GuestRenderState RenderState);
internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw,
- IReadOnlyList Targets,
+ IReadOnlyList Targets,
bool PublishTarget);
internal sealed record VulkanComputeGuestDispatch(
ulong ShaderAddress,
byte[] ComputeSpirv,
- IReadOnlyList Textures,
- IReadOnlyList GlobalMemoryBuffers,
+ IReadOnlyList Textures,
+ IReadOnlyList GlobalMemoryBuffers,
uint GroupCountX,
uint GroupCountY,
uint GroupCountZ);
@@ -407,8 +305,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitTranslatedDraw(
byte[] pixelSpirv,
- IReadOnlyList textures,
- IReadOnlyList globalMemoryBuffers,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
uint width,
uint height,
uint attributeCount,
@@ -416,9 +314,9 @@ internal static unsafe class VulkanVideoPresenter
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
- VulkanGuestIndexBuffer? indexBuffer = null,
- IReadOnlyList? vertexBuffers = null,
- VulkanGuestRenderState? renderState = null)
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 || width == 0 || height == 0)
{
@@ -456,7 +354,7 @@ internal static unsafe class VulkanVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
- renderState ?? VulkanGuestRenderState.Default),
+ renderState ?? GuestRenderState.Default),
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
IsSplash: false);
System.Threading.Monitor.PulseAll(_gate);
@@ -473,17 +371,17 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
- IReadOnlyList textures,
- IReadOnlyList globalMemoryBuffers,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
uint attributeCount,
- VulkanGuestRenderTarget target,
+ GuestRenderTarget target,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
- VulkanGuestIndexBuffer? indexBuffer = null,
- IReadOnlyList? vertexBuffers = null,
- VulkanGuestRenderState? renderState = null)
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null)
{
SubmitOffscreenTranslatedDraw(
pixelSpirv,
@@ -502,17 +400,17 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
- IReadOnlyList textures,
- IReadOnlyList globalMemoryBuffers,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
uint attributeCount,
- IReadOnlyList targets,
+ IReadOnlyList targets,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
- VulkanGuestIndexBuffer? indexBuffer = null,
- IReadOnlyList? vertexBuffers = null,
- VulkanGuestRenderState? renderState = null)
+ GuestIndexBuffer? indexBuffer = null,
+ IReadOnlyList? vertexBuffers = null,
+ GuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 ||
targets.Count == 0 ||
@@ -541,7 +439,7 @@ internal static unsafe class VulkanVideoPresenter
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
}
- var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
+ var effectiveRenderState = renderState ?? GuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{
effectiveRenderState = effectiveRenderState with
@@ -589,8 +487,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitStorageTranslatedDraw(
byte[] pixelSpirv,
- IReadOnlyList textures,
- IReadOnlyList globalMemoryBuffers,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
uint attributeCount,
uint width,
uint height)
@@ -623,8 +521,8 @@ internal static unsafe class VulkanVideoPresenter
1,
4,
null,
- VulkanGuestRenderState.Default),
- [new VulkanGuestRenderTarget(
+ GuestRenderState.Default),
+ [new GuestRenderTarget(
Address: 0,
width,
height,
@@ -637,8 +535,8 @@ internal static unsafe class VulkanVideoPresenter
public static void SubmitComputeDispatch(
ulong shaderAddress,
byte[] computeSpirv,
- IReadOnlyList textures,
- IReadOnlyList globalMemoryBuffers,
+ IReadOnlyList textures,
+ IReadOnlyList globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ)
@@ -818,7 +716,7 @@ internal static unsafe class VulkanVideoPresenter
SubmitOffscreenTranslatedDraw(
fragmentSpirv,
[
- new VulkanGuestDrawTexture(
+ new GuestDrawTexture(
sourceAddress,
sourceWidth,
sourceHeight,
@@ -830,7 +728,7 @@ internal static unsafe class VulkanVideoPresenter
],
[],
attributeCount: 1,
- new VulkanGuestRenderTarget(
+ new GuestRenderTarget(
destinationAddress,
destinationWidth,
destinationHeight,
@@ -1363,7 +1261,7 @@ internal static unsafe class VulkanVideoPresenter
private readonly Dictionary _computePipelines =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary _graphicsPipelines = new();
- private readonly Dictionary _samplers = new();
+ private readonly Dictionary _samplers = new();
private readonly Dictionary _shaderDigests =
new(ReferenceEqualityComparer.Instance);
private readonly Dictionary
@@ -1418,9 +1316,9 @@ internal static unsafe class VulkanVideoPresenter
public uint VertexCount = 3;
public uint InstanceCount = 1;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
- public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
- public VulkanGuestRect? Scissor;
- public VulkanGuestViewport? Viewport;
+ public GuestBlendState[] Blends = [GuestBlendState.Default];
+ public GuestRect? Scissor;
+ public GuestViewport? Viewport;
public RenderPass TransientRenderPass;
public Framebuffer TransientFramebuffer;
}
@@ -1440,7 +1338,7 @@ internal static unsafe class VulkanVideoPresenter
public bool NeedsUpload;
public bool OwnsStorage;
public bool IsStorage;
- public VulkanGuestSampler SamplerState;
+ public GuestSampler SamplerState;
public Sampler Sampler;
public GuestImageResource? GuestImage;
public ulong CpuContentFingerprint;
@@ -1740,11 +1638,11 @@ internal static unsafe class VulkanVideoPresenter
$"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}";
}
- private static string GuestImageDebugName(VulkanGuestRenderTarget target, Format format) =>
+ private static string GuestImageDebugName(GuestRenderTarget target, Format format) =>
$"SharpEmu guest 0x{target.Address:X16} {target.Width}x{target.Height} " +
$"fmt{target.Format}/{format}";
- private static string TextureDebugName(VulkanGuestDrawTexture texture, Format format) =>
+ private static string TextureDebugName(GuestDrawTexture texture, Format format) =>
$"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " +
$"fmt{texture.Format}/{format}";
@@ -3621,7 +3519,7 @@ internal static unsafe class VulkanVideoPresenter
}
[MethodImpl(MethodImplOptions.NoInlining)]
- private TextureResource ResolveTextureResource(VulkanGuestDrawTexture texture)
+ private TextureResource ResolveTextureResource(GuestDrawTexture texture)
{
if (texture.IsStorage)
{
@@ -3695,7 +3593,7 @@ internal static unsafe class VulkanVideoPresenter
}
private bool TryCreateCpuTextureRefreshResource(
- VulkanGuestDrawTexture texture,
+ GuestDrawTexture texture,
GuestImageResource guestImage,
ImageView view,
out TextureResource resource)
@@ -3760,7 +3658,7 @@ internal static unsafe class VulkanVideoPresenter
}
private static bool IsCompatibleGuestImageAlias(
- VulkanGuestDrawTexture texture,
+ GuestDrawTexture texture,
GuestImageResource guestImage)
{
if (guestImage.Width == texture.Width &&
@@ -3781,7 +3679,7 @@ internal static unsafe class VulkanVideoPresenter
}
[MethodImpl(MethodImplOptions.NoInlining)]
- private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture)
+ private TextureResource ResolveStorageImageResource(GuestDrawTexture texture)
{
if (texture.Address == 0)
{
@@ -3858,7 +3756,7 @@ internal static unsafe class VulkanVideoPresenter
return resource;
}
- private TextureResource CreateStorageScratchResource(VulkanGuestDrawTexture texture)
+ private TextureResource CreateStorageScratchResource(GuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
@@ -3948,7 +3846,7 @@ internal static unsafe class VulkanVideoPresenter
};
}
- private GuestImageResource ResolveStorageGuestImage(VulkanGuestDrawTexture texture)
+ private GuestImageResource ResolveStorageGuestImage(GuestDrawTexture texture)
{
if (texture.Address == 0)
{
@@ -3957,7 +3855,7 @@ internal static unsafe class VulkanVideoPresenter
var format = GetTextureFormat(texture.Format, texture.NumberType);
var guestImage = GetOrCreateGuestImage(
- new VulkanGuestRenderTarget(
+ new GuestRenderTarget(
texture.Address,
texture.Width,
texture.Height,
@@ -3974,7 +3872,7 @@ internal static unsafe class VulkanVideoPresenter
return guestImage;
}
- private TextureResource CreateTextureResource(VulkanGuestDrawTexture texture)
+ private TextureResource CreateTextureResource(GuestDrawTexture texture)
{
var width = Math.Max(texture.Width, 1);
var height = Math.Max(texture.Height, 1);
@@ -4172,7 +4070,7 @@ internal static unsafe class VulkanVideoPresenter
}
private void DumpTextureUpload(
- VulkanGuestDrawTexture texture,
+ GuestDrawTexture texture,
byte[] pixels,
uint rowLength,
uint width,
@@ -4264,7 +4162,7 @@ internal static unsafe class VulkanVideoPresenter
private static void WriteInt32(byte[] output, int offset, int value) =>
WriteUInt32(output, offset, unchecked((uint)value));
- private Sampler CreateSampler(VulkanGuestSampler sampler)
+ private Sampler CreateSampler(GuestSampler sampler)
{
if (_samplers.TryGetValue(sampler, out var cachedSampler))
{
@@ -4342,7 +4240,7 @@ internal static unsafe class VulkanVideoPresenter
}
private GlobalBufferResource CreateGlobalBufferResource(
- VulkanGuestMemoryBuffer guestBuffer)
+ GuestMemoryBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
@@ -4371,7 +4269,7 @@ internal static unsafe class VulkanVideoPresenter
}
private VertexBufferResource CreateVertexBufferResource(
- VulkanGuestVertexBuffer guestBuffer)
+ GuestVertexBuffer guestBuffer)
{
var buffer = CreateHostBuffer(
guestBuffer.Data,
@@ -4594,7 +4492,7 @@ internal static unsafe class VulkanVideoPresenter
private static uint GetDrawVertexCount(
uint primitiveType,
uint vertexCount,
- VulkanGuestIndexBuffer? indexBuffer)
+ GuestIndexBuffer? indexBuffer)
{
if (primitiveType == GuestPrimitiveRectList && indexBuffer is null)
{
@@ -4640,41 +4538,41 @@ internal static unsafe class VulkanVideoPresenter
_ => BlendOp.Add,
};
- private static uint DecodeSamplerClampX(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerClampX(GuestSampler sampler) =>
sampler.Word0 & 0x7u;
- private static uint DecodeSamplerClampY(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerClampY(GuestSampler sampler) =>
(sampler.Word0 >> 3) & 0x7u;
- private static uint DecodeSamplerClampZ(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerClampZ(GuestSampler sampler) =>
(sampler.Word0 >> 6) & 0x7u;
- private static uint DecodeSamplerDepthCompare(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerDepthCompare(GuestSampler sampler) =>
(sampler.Word0 >> 12) & 0x7u;
- private static float DecodeSamplerMinLod(VulkanGuestSampler sampler) =>
+ private static float DecodeSamplerMinLod(GuestSampler sampler) =>
(sampler.Word1 & 0xFFFu) / 256.0f;
- private static float DecodeSamplerMaxLod(VulkanGuestSampler sampler) =>
+ private static float DecodeSamplerMaxLod(GuestSampler sampler) =>
((sampler.Word1 >> 12) & 0xFFFu) / 256.0f;
- private static float DecodeSamplerLodBias(VulkanGuestSampler sampler)
+ private static float DecodeSamplerLodBias(GuestSampler sampler)
{
var raw = sampler.Word2 & 0x3FFFu;
var signed = (short)((raw ^ 0x2000u) - 0x2000u);
return signed / 256.0f;
}
- private static uint DecodeSamplerMagFilter(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerMagFilter(GuestSampler sampler) =>
(sampler.Word2 >> 20) & 0x3u;
- private static uint DecodeSamplerMinFilter(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerMinFilter(GuestSampler sampler) =>
(sampler.Word2 >> 22) & 0x3u;
- private static uint DecodeSamplerMipFilter(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerMipFilter(GuestSampler sampler) =>
(sampler.Word2 >> 26) & 0x3u;
- private static uint DecodeSamplerBorderColor(VulkanGuestSampler sampler) =>
+ private static uint DecodeSamplerBorderColor(GuestSampler sampler) =>
(sampler.Word3 >> 30) & 0x3u;
private static SamplerAddressMode ToVkSamplerAddressMode(uint mode) =>
@@ -4741,11 +4639,11 @@ internal static unsafe class VulkanVideoPresenter
return flags;
}
- private static VulkanGuestRect ClampScissor(VulkanGuestRect? scissor, Extent2D extent)
+ private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
{
if (scissor is not { } rect)
{
- return new VulkanGuestRect(0, 0, extent.Width, extent.Height);
+ return new GuestRect(0, 0, extent.Width, extent.Height);
}
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
@@ -4758,7 +4656,7 @@ internal static unsafe class VulkanVideoPresenter
rect.Y + checked((int)rect.Height),
top,
checked((int)extent.Height));
- return new VulkanGuestRect(
+ return new GuestRect(
left,
top,
checked((uint)(right - left)),
@@ -4773,7 +4671,7 @@ internal static unsafe class VulkanVideoPresenter
? viewportEpsilon
: 0f;
- private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent)
+ private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
{
if (viewport is not { } rect)
{
@@ -5476,7 +5374,7 @@ internal static unsafe class VulkanVideoPresenter
[MethodImpl(MethodImplOptions.NoInlining)]
private GuestImageResource GetOrCreateGuestImage(
- VulkanGuestRenderTarget target,
+ GuestRenderTarget target,
Format format)
{
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
@@ -5999,15 +5897,15 @@ internal static unsafe class VulkanVideoPresenter
var gpuInFlight = _pendingGuestSubmissions.Count +
(_presentationInFlight ? 1 : 0);
var readCount = Interlocked.Read(
- ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
+ ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
var readBytes = Interlocked.Read(
- ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
+ ref Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
var readHits = Interlocked.Read(
- ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
+ ref Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
var readPvmBytes = Interlocked.Read(
- ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
+ ref Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
var readLibcBytes = Interlocked.Read(
- ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
+ ref Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
var readsPerSecond =
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
var readMbPerSecond =
diff --git a/src/SharpEmu.Libs/packages.lock.json b/src/SharpEmu.Libs/packages.lock.json
index efa61e0..1ae3bd0 100644
--- a/src/SharpEmu.Libs/packages.lock.json
+++ b/src/SharpEmu.Libs/packages.lock.json
@@ -127,11 +127,23 @@
"sharpemu.hle": {
"type": "Project",
"dependencies": {
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.logging": {
"type": "Project"
+ },
+ "sharpemu.shadercompiler": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.HLE": "[0.0.1, )"
+ }
+ },
+ "sharpemu.shadercompiler.vulkan": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.ShaderCompiler": "[0.0.1, )"
+ }
}
}
}
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvShader.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvShader.cs
new file mode 100644
index 0000000..7bfac43
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvShader.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.ShaderCompiler;
+
+namespace SharpEmu.ShaderCompiler.Vulkan;
+
+// SPIR-V-specific shader artifact types. These stay beside the SPIR-V emitter (not in
+// the backend-neutral SharpEmu.ShaderCompiler project): each codegen owns its own
+// compiled-shader shape.
+public enum Gen5SpirvStage
+{
+ Vertex,
+ Pixel,
+ Compute,
+}
+
+public sealed record Gen5SpirvShader(
+ byte[] Spirv,
+ IReadOnlyList GlobalMemoryBindings,
+ IReadOnlyList ImageBindings,
+ uint AttributeCount,
+ IReadOnlyList VertexInputs);
diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
similarity index 98%
rename from src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs
rename to src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
index 46d36a5..6fbf059 100644
--- a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
@@ -1,9 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-namespace SharpEmu.Libs.Agc;
+using SharpEmu.ShaderCompiler;
-internal static partial class Gen5SpirvTranslator
+namespace SharpEmu.ShaderCompiler.Vulkan;
+
+public static partial class Gen5SpirvTranslator
{
private sealed partial class CompilationContext
{
@@ -2491,47 +2493,7 @@ internal static partial class Gen5SpirvTranslator
resultSignChanged);
}
- private static bool TryDecodeInlineConstant(uint encoded, out uint value)
- {
- if (encoded == 125)
- {
- value = 0;
- return true;
- }
-
- if (encoded is >= 128 and <= 192)
- {
- value = encoded - 128;
- return true;
- }
-
- if (encoded is >= 193 and <= 208)
- {
- value = unchecked((uint)-(int)(encoded - 192));
- return true;
- }
-
- var floatingPoint = encoded switch
- {
- 240 => 0.5f,
- 241 => -0.5f,
- 242 => 1.0f,
- 243 => -1.0f,
- 244 => 2.0f,
- 245 => -2.0f,
- 246 => 4.0f,
- 247 => -4.0f,
- 248 => 1.0f / (2.0f * MathF.PI),
- _ => float.NaN,
- };
- if (float.IsNaN(floatingPoint))
- {
- value = 0;
- return false;
- }
-
- value = BitConverter.SingleToUInt32Bits(floatingPoint);
- return true;
- }
+ private static bool TryDecodeInlineConstant(uint encoded, out uint value) =>
+ Gen5InlineConstants.TryDecode(encoded, out value);
}
}
diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
similarity index 99%
rename from src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs
rename to src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
index cb19c0a..34478ee 100644
--- a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
@@ -1,9 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-namespace SharpEmu.Libs.Agc;
+using SharpEmu.ShaderCompiler;
-internal static partial class Gen5SpirvTranslator
+namespace SharpEmu.ShaderCompiler.Vulkan;
+
+public static partial class Gen5SpirvTranslator
{
private const uint ScalarRegisterCount = 256;
private const uint VectorRegisterCount = 512;
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj b/src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj
new file mode 100644
index 0000000..200fdf1
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
similarity index 98%
rename from src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs
rename to src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
index a2cae91..7626886 100644
--- a/src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvFixedShaders.cs
@@ -1,9 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler.Vulkan;
-internal static class SpirvFixedShaders
+public static class SpirvFixedShaders
{
public static byte[] CreateFullscreenVertex(uint attributeCount)
{
diff --git a/src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
similarity index 98%
rename from src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs
rename to src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
index c6394a9..bbb7b0b 100644
--- a/src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
@@ -4,9 +4,9 @@
using System.Buffers.Binary;
using System.Text;
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler.Vulkan;
-internal enum SpirvOp : ushort
+public enum SpirvOp : ushort
{
Nop = 0,
Name = 5,
@@ -167,7 +167,7 @@ internal enum SpirvOp : ushort
GroupNonUniformShuffleDown = 348,
}
-internal enum SpirvCapability : uint
+public enum SpirvCapability : uint
{
Shader = 1,
Float16 = 9,
@@ -186,7 +186,7 @@ internal enum SpirvCapability : uint
RuntimeDescriptorArray = 5302,
}
-internal enum SpirvStorageClass : uint
+public enum SpirvStorageClass : uint
{
UniformConstant = 0,
Input = 1,
@@ -200,21 +200,21 @@ internal enum SpirvStorageClass : uint
StorageBuffer = 12,
}
-internal enum SpirvExecutionModel : uint
+public enum SpirvExecutionModel : uint
{
Vertex = 0,
Fragment = 4,
GLCompute = 5,
}
-internal enum SpirvExecutionMode : uint
+public enum SpirvExecutionMode : uint
{
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
}
-internal enum SpirvDecoration : uint
+public enum SpirvDecoration : uint
{
Block = 2,
ArrayStride = 6,
@@ -227,7 +227,7 @@ internal enum SpirvDecoration : uint
Offset = 35,
}
-internal enum SpirvBuiltIn : uint
+public enum SpirvBuiltIn : uint
{
Position = 0,
VertexIndex = 42,
@@ -241,7 +241,7 @@ internal enum SpirvBuiltIn : uint
SubgroupLocalInvocationId = 41,
}
-internal enum SpirvImageDim : uint
+public enum SpirvImageDim : uint
{
Dim1D = 0,
Dim2D = 1,
@@ -250,7 +250,7 @@ internal enum SpirvImageDim : uint
Buffer = 5,
}
-internal enum SpirvImageFormat : uint
+public enum SpirvImageFormat : uint
{
Unknown = 0,
Rgba32f = 1,
@@ -294,7 +294,7 @@ internal enum SpirvImageFormat : uint
R8ui = 39,
}
-internal sealed class SpirvModuleBuilder
+public sealed class SpirvModuleBuilder
{
private const uint Magic = 0x07230203;
private const uint Version15 = 0x00010500;
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/packages.lock.json b/src/SharpEmu.ShaderCompiler.Vulkan/packages.lock.json
new file mode 100644
index 0000000..8072d97
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/packages.lock.json
@@ -0,0 +1,22 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "sharpemu.hle": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.Logging": "[0.0.1, )"
+ }
+ },
+ "sharpemu.logging": {
+ "type": "Project"
+ },
+ "sharpemu.shadercompiler": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.HLE": "[0.0.1, )"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs b/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs
new file mode 100644
index 0000000..672d80b
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs
@@ -0,0 +1,54 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.ShaderCompiler;
+
+///
+/// The Gen5 (gfx10) inline-constant operand table, shared by every codegen so backends
+/// cannot drift on constant semantics.
+///
+public static class Gen5InlineConstants
+{
+ public static bool TryDecode(uint encoded, out uint value)
+ {
+ if (encoded == 125)
+ {
+ value = 0;
+ return true;
+ }
+
+ if (encoded is >= 128 and <= 192)
+ {
+ value = encoded - 128;
+ return true;
+ }
+
+ if (encoded is >= 193 and <= 208)
+ {
+ value = unchecked((uint)-(int)(encoded - 192));
+ return true;
+ }
+
+ var floatingPoint = encoded switch
+ {
+ 240 => 0.5f,
+ 241 => -0.5f,
+ 242 => 1.0f,
+ 243 => -1.0f,
+ 244 => 2.0f,
+ 245 => -2.0f,
+ 246 => 4.0f,
+ 247 => -4.0f,
+ 248 => 1.0f / (2.0f * MathF.PI),
+ _ => float.NaN,
+ };
+ if (float.IsNaN(floatingPoint))
+ {
+ value = 0;
+ return false;
+ }
+
+ value = BitConverter.SingleToUInt32Bits(floatingPoint);
+ return true;
+ }
+}
diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
similarity index 80%
rename from src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs
rename to src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
index d0a5d58..ba0db21 100644
--- a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
@@ -1,9 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler;
-internal enum Gen5ShaderEncoding
+public enum Gen5ShaderEncoding
{
Sop1,
Sop2,
@@ -26,7 +26,7 @@ internal enum Gen5ShaderEncoding
Exp,
}
-internal enum Gen5OperandKind
+public enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
@@ -34,7 +34,7 @@ internal enum Gen5OperandKind
LiteralConstant,
}
-internal enum Gen5ShaderResourceKind
+public enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
@@ -42,45 +42,31 @@ internal enum Gen5ShaderResourceKind
ConstantBuffer,
}
-internal enum Gen5PixelOutputKind
+public enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
-internal readonly record struct Gen5PixelOutputBinding(
+public readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
-internal enum Gen5SpirvStage
-{
- Vertex,
- Pixel,
- Compute,
-}
-
-internal sealed record Gen5SpirvShader(
- byte[] Spirv,
- IReadOnlyList GlobalMemoryBindings,
- IReadOnlyList ImageBindings,
- uint AttributeCount,
- IReadOnlyList VertexInputs);
-
-internal readonly record struct Gen5ShaderResourceMapping(
+public readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
-internal sealed record Gen5ShaderMetadata(
+public sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary DirectResources,
IReadOnlyList Resources);
-internal readonly record struct Gen5ComputeSystemRegisters(
+public readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
@@ -133,14 +119,14 @@ internal readonly record struct Gen5ComputeSystemRegisters(
}
}
-internal sealed record Gen5ShaderState(
+public sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
-internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
+public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
@@ -177,9 +163,9 @@ internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
};
}
-internal abstract record Gen5InstructionControl;
+public abstract record Gen5InstructionControl;
-internal sealed record Gen5ImageControl(
+public sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList AddressRegisters,
@@ -197,7 +183,7 @@ internal sealed record Gen5ImageControl(
: VectorAddress + (uint)component;
}
-internal sealed record Gen5GlobalMemoryControl(
+public sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
@@ -206,7 +192,7 @@ internal sealed record Gen5GlobalMemoryControl(
bool Glc,
bool Slc) : Gen5InstructionControl;
-internal sealed record Gen5BufferMemoryControl(
+public sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
@@ -217,25 +203,25 @@ internal sealed record Gen5BufferMemoryControl(
bool Glc,
bool Slc) : Gen5InstructionControl;
-internal sealed record Gen5ExportControl(
+public sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
-internal sealed record Gen5InterpolationControl(
+public sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
-internal sealed record Gen5Vop3Control(
+public sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
-internal sealed record Gen5SdwaControl(
+public sealed record Gen5SdwaControl(
uint DestinationSelect,
uint Source0Select,
uint Source1Select,
@@ -244,7 +230,7 @@ internal sealed record Gen5SdwaControl(
uint OutputModifier,
bool Clamp) : Gen5InstructionControl;
-internal sealed record Gen5DppControl(
+public sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
@@ -253,17 +239,17 @@ internal sealed record Gen5DppControl(
uint BankMask,
uint RowMask) : Gen5InstructionControl;
-internal sealed record Gen5ScalarMemoryControl(
+public sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
-internal sealed record Gen5DataShareControl(
+public sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
-internal sealed record Gen5ImageBinding(
+public sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
@@ -271,13 +257,13 @@ internal sealed record Gen5ImageBinding(
IReadOnlyList SamplerDescriptor,
uint? MipLevel);
-internal sealed record Gen5GlobalMemoryBinding(
+public sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList InstructionPcs,
byte[] Data);
-internal sealed record Gen5VertexInputBinding(
+public sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
@@ -288,7 +274,7 @@ internal sealed record Gen5VertexInputBinding(
uint OffsetBytes,
byte[] Data);
-internal sealed record Gen5ShaderEvaluation(
+public sealed record Gen5ShaderEvaluation(
IReadOnlyList InitialScalarRegisters,
IReadOnlyList ScalarRegisters,
IReadOnlyDictionary> ScalarRegistersByPc,
@@ -298,7 +284,7 @@ internal sealed record Gen5ShaderEvaluation(
IReadOnlySet? RuntimeScalarRegisters = null,
IReadOnlyList? VertexInputs = null);
-internal sealed record Gen5ShaderInstruction(
+public sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
@@ -307,7 +293,7 @@ internal sealed record Gen5ShaderInstruction(
IReadOnlyList Destinations,
Gen5InstructionControl? Control);
-internal sealed record Gen5ShaderProgram(
+public sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList Instructions)
{
diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderMetadataReader.cs
similarity index 97%
rename from src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs
rename to src/SharpEmu.ShaderCompiler/Gen5ShaderMetadataReader.cs
index 87846ec..9880f66 100644
--- a/src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderMetadataReader.cs
@@ -3,9 +3,9 @@
using SharpEmu.HLE;
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler;
-internal static class Gen5ShaderMetadataReader
+public static class Gen5ShaderMetadataReader
{
private const ulong ShaderUserDataOffset = 0x08;
private const int ResourceClassCount = 4;
diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs
similarity index 98%
rename from src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs
rename to src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs
index 62db7a1..520fa3a 100644
--- a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs
@@ -2,13 +2,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
-using SharpEmu.Libs.Kernel;
using System.Numerics;
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler;
-internal static class Gen5ShaderScalarEvaluator
+public static class Gen5ShaderScalarEvaluator
{
+ ///
+ /// Optional fallback for global-memory reads that ctx.Memory cannot satisfy (the
+ /// emulator installs the HLE-tracked libc heap reader here at module load). Kept as
+ /// a hook so this project never depends on the HLE module implementations.
+ ///
+ public static Gen5FallbackMemoryReader? FallbackMemoryReader { get; set; }
+
+ public delegate bool Gen5FallbackMemoryReader(ulong baseAddress, Span destination);
+
private const int ScalarRegisterCount = 256;
private const int ImageDescriptorDwords = 8;
private const int SamplerDescriptorDwords = 4;
@@ -20,12 +28,12 @@ internal static class Gen5ShaderScalarEvaluator
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
: 1 * 1024 * 1024;
- internal static long GlobalMemoryReadCount;
- internal static long GlobalMemoryReadBytes;
- internal static long GlobalMemoryReadCacheHits;
- internal static long GlobalMemoryReadPvmBytes;
- internal static long GlobalMemoryReadLibcBytes;
- internal static long GlobalMemoryReadReuses;
+ public static long GlobalMemoryReadCount;
+ public static long GlobalMemoryReadBytes;
+ public static long GlobalMemoryReadCacheHits;
+ public static long GlobalMemoryReadPvmBytes;
+ public static long GlobalMemoryReadLibcBytes;
+ public static long GlobalMemoryReadReuses;
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
private static readonly object _crossFrameReadGate = new();
@@ -573,12 +581,12 @@ internal static class Gen5ShaderScalarEvaluator
[ThreadStatic]
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
- internal static void BeginGlobalMemoryReadScope()
+ public static void BeginGlobalMemoryReadScope()
{
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
}
- internal static void EndGlobalMemoryReadScope()
+ public static void EndGlobalMemoryReadScope()
{
_globalMemoryReadCache = null;
}
@@ -647,7 +655,7 @@ internal static class Gen5ShaderScalarEvaluator
data = GC.AllocateUninitializedArray(candidateSize);
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
if (readFromPvm ||
- KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
+ FallbackMemoryReader?.Invoke(baseAddress, data) == true)
{
Interlocked.Increment(ref GlobalMemoryReadCount);
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
similarity index 99%
rename from src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs
rename to src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
index e6db37d..90df507 100644
--- a/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
@@ -2,14 +2,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
-using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Text;
-namespace SharpEmu.Libs.Agc;
+namespace SharpEmu.ShaderCompiler;
-internal static class Gen5ShaderTranslator
+public static class Gen5ShaderTranslator
{
private const int MaxInstructions = 4096;
private const int MinimumUserDataDwords = 16;
@@ -276,7 +275,9 @@ internal static class Gen5ShaderTranslator
return true;
}
- private static bool TryDecodeProgram(
+ // Public contract entry: emitter test suites and tools drive the decoder directly
+ // from raw instruction words.
+ public static bool TryDecodeProgram(
CpuContext ctx,
ulong address,
out Gen5ShaderProgram program,
@@ -1220,7 +1221,7 @@ internal static class Gen5ShaderTranslator
private static bool IsMimgInstruction(string name) =>
name.StartsWith("Image", StringComparison.Ordinal);
- internal static bool IsStorageImageOperation(string name) =>
+ public static bool IsStorageImageOperation(string name) =>
name.StartsWith("ImageStore", StringComparison.Ordinal) ||
name.StartsWith("ImageAtomic", StringComparison.Ordinal);
diff --git a/src/SharpEmu.ShaderCompiler/GuestDrawKind.cs b/src/SharpEmu.ShaderCompiler/GuestDrawKind.cs
new file mode 100644
index 0000000..7696f85
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler/GuestDrawKind.cs
@@ -0,0 +1,15 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.ShaderCompiler;
+
+///
+/// Guest draw patterns the decoder recognizes from known shader programs. Guest-domain,
+/// backend-neutral — it previously lived inside the Vulkan presenter, which is exactly
+/// the kind of placement this project exists to prevent.
+///
+public enum GuestDrawKind
+{
+ None,
+ FullscreenBarycentric,
+}
diff --git a/src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj b/src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj
new file mode 100644
index 0000000..cc3f89f
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.ShaderCompiler/packages.lock.json b/src/SharpEmu.ShaderCompiler/packages.lock.json
new file mode 100644
index 0000000..6dee471
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler/packages.lock.json
@@ -0,0 +1,16 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "sharpemu.hle": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.Logging": "[0.0.1, )"
+ }
+ },
+ "sharpemu.logging": {
+ "type": "Project"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SharpEmu.Libs.Tests/packages.lock.json b/tests/SharpEmu.Libs.Tests/packages.lock.json
index 82aca41..9004c75 100644
--- a/tests/SharpEmu.Libs.Tests/packages.lock.json
+++ b/tests/SharpEmu.Libs.Tests/packages.lock.json
@@ -170,21 +170,23 @@
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
- "SharpEmu.HLE": "[1.0.0, )",
- "SharpEmu.Libs": "[1.0.0, )",
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.HLE": "[0.0.1, )",
+ "SharpEmu.Libs": "[0.0.1, )",
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
- "SharpEmu.Logging": "[1.0.0, )"
+ "SharpEmu.Logging": "[0.0.1, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
- "SharpEmu.HLE": "[1.0.0, )",
+ "SharpEmu.HLE": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler": "[0.0.1, )",
+ "SharpEmu.ShaderCompiler.Vulkan": "[0.0.1, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
@@ -195,6 +197,18 @@
"sharpemu.logging": {
"type": "Project"
},
+ "sharpemu.shadercompiler": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.HLE": "[0.0.1, )"
+ }
+ },
+ "sharpemu.shadercompiler.vulkan": {
+ "type": "Project",
+ "dependencies": {
+ "SharpEmu.ShaderCompiler": "[0.0.1, )"
+ }
+ },
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
diff --git a/tools/SharpEmu.Tools.ShaderDump/Program.cs b/tools/SharpEmu.Tools.ShaderDump/Program.cs
index 6ec1e69..5da8bcf 100644
--- a/tools/SharpEmu.Tools.ShaderDump/Program.cs
+++ b/tools/SharpEmu.Tools.ShaderDump/Program.cs
@@ -4,10 +4,9 @@
// Synthetic-shader conformance dumper.
//
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real
-// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
-// reflection so no emulator source changes are required) and writes the
-// resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be
-// checked with spirv-val / spirv-dis.
+// decode -> SPIR-V pipeline (SharpEmu.ShaderCompiler + SharpEmu.ShaderCompiler.Vulkan)
+// and writes the resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs
+// can then be checked with spirv-val / spirv-dis.
//
// Programs that contain buffer_store_dword automatically get a single
// global-memory binding covering every store, which the emitter exposes as
@@ -21,9 +20,9 @@
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
using System.Buffers.Binary;
-using System.Reflection;
using SharpEmu.HLE;
-using SharpEmu.Libs.CxxAbi;
+using SharpEmu.ShaderCompiler;
+using SharpEmu.ShaderCompiler.Vulkan;
const ulong ProgramAddress = 0x100000;
@@ -138,44 +137,6 @@ const ulong ProgramAddress = 0x100000;
]),
];
-var assembly = typeof(CxaGuardExports).Assembly;
-var shaderTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderTranslator")
- ?? throw new InvalidOperationException("Gen5ShaderTranslator not found");
-var spirvTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5SpirvTranslator")
- ?? throw new InvalidOperationException("Gen5SpirvTranslator not found");
-var describe = shaderTranslator.GetMethod(
- "Describe",
- BindingFlags.Public | BindingFlags.Static)
- ?? throw new InvalidOperationException("Gen5ShaderTranslator.Describe not found");
-var tryDecode = shaderTranslator.GetMethod(
- "TryDecodeProgram",
- BindingFlags.NonPublic | BindingFlags.Static)
- ?? throw new InvalidOperationException("Gen5ShaderTranslator.TryDecodeProgram not found");
-var stateType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderState")
- ?? throw new InvalidOperationException("Gen5ShaderState not found");
-var evaluationType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderEvaluation")
- ?? throw new InvalidOperationException("Gen5ShaderEvaluation not found");
-var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
- ?? throw new InvalidOperationException("Gen5ImageBinding not found");
-var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
- ?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
-var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding")
- ?? throw new InvalidOperationException("Gen5PixelOutputBinding not found");
-var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind")
- ?? throw new InvalidOperationException("Gen5PixelOutputKind not found");
-var tryCompile = spirvTranslator.GetMethod(
- "TryCompileVertexShader",
- BindingFlags.Public | BindingFlags.Static)
- ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
-var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static)
- .Single(method =>
- method.Name == "TryCompilePixelShader" &&
- method.GetParameters()[2].ParameterType.IsGenericType);
-var tryCompileCompute = spirvTranslator.GetMethod(
- "TryCompileComputeShader",
- BindingFlags.Public | BindingFlags.Static)
- ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileComputeShader not found");
-
var outputDirectory = args.Length > 0
? args[0]
: Path.Combine(AppContext.BaseDirectory, "spv");
@@ -190,19 +151,18 @@ foreach (var (name, expectTranslate, words) in testPrograms)
Console.WriteLine(
$"[{name}] decode: " +
- (string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!);
+ Gen5ShaderTranslator.Describe(ctx, ProgramAddress, ProgramAddress));
- object?[] decodeArgs = [ctx, ProgramAddress, null, null];
- if (!(bool)tryDecode.Invoke(null, decodeArgs)!)
+ if (!Gen5ShaderTranslator.TryDecodeProgram(ctx, ProgramAddress, out var program, out var decodeError))
{
if (expectTranslate)
{
failures++;
- Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})");
+ Console.WriteLine($"[{name}] FAILED: decode error ({decodeError})");
}
else
{
- Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})");
+ Console.WriteLine($"[{name}] decode failed as expected ({decodeError})");
}
continue;
@@ -219,167 +179,110 @@ foreach (var (name, expectTranslate, words) in testPrograms)
// Buffer stores need a global-memory binding; the emitter resolves them by
// instruction PC, so collect store PCs from the decoded program itself.
- var programObj = decodeArgs[2]!;
- var instructions = (System.Collections.IEnumerable)programObj
- .GetType().GetProperty("Instructions")!.GetValue(programObj)!;
var storePcs = new List();
- foreach (var instruction in instructions)
+ foreach (var instruction in program!.Instructions)
{
- var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!;
- if (op.StartsWith("BufferStore", StringComparison.Ordinal))
+ if (instruction.Opcode.StartsWith("BufferStore", StringComparison.Ordinal))
{
- storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!);
+ storePcs.Add(instruction.Pc);
}
}
// The binding's scalar base (8 -> s[8:11]) must match the srsrc field of
// the hand-assembled buffer_store words, and the 64-byte backing store
// must cover every hand-assembled store offset.
- var globalBindings = Array.CreateInstance(globalBindingType, storePcs.Count > 0 ? 1 : 0);
- if (storePcs.Count > 0)
- {
- globalBindings.SetValue(
- Activator.CreateInstance(
- globalBindingType,
- 8u,
- 0UL,
- (IReadOnlyList)storePcs,
- new byte[64]),
- 0);
- }
+ var globalBindings = storePcs.Count > 0
+ ? new[] { new Gen5GlobalMemoryBinding(8u, 0UL, storePcs, new byte[64]) }
+ : Array.Empty();
- var state = Activator.CreateInstance(
- stateType,
- programObj,
- new uint[16],
- null,
- null,
- 0u)!;
- var evaluation = Activator.CreateInstance(
- evaluationType,
+ var state = new Gen5ShaderState(program, new uint[16], Metadata: null);
+ var evaluation = new Gen5ShaderEvaluation(
new uint[256],
new uint[256],
new Dictionary>(),
- Array.CreateInstance(imageBindingType, 0),
- globalBindings,
- null,
- null,
- null)!;
+ Array.Empty(),
+ globalBindings);
- var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]);
- if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!)
+ if (Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var vertexShader, out var vertexError))
{
- var shader = compileArgs[2]!;
- var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}.spv");
- File.WriteAllBytes(path, spirv);
- Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}");
+ File.WriteAllBytes(path, vertexShader.Spirv);
+ Console.WriteLine($"[{name}] emit: success, {vertexShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
- Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})");
+ Console.WriteLine($"[{name}] emit: FAILED ({vertexError})");
}
- var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]);
- if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!)
+ if (Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, 1, 1, 1, out var computeShader, out var computeError))
{
- var shader = computeArgs[5]!;
- var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}-cs.spv");
- File.WriteAllBytes(path, spirv);
- Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}");
+ File.WriteAllBytes(path, computeShader.Spirv);
+ Console.WriteLine($"[{name}] compute emit: success, {computeShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
- Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
+ Console.WriteLine($"[{name}] compute emit: FAILED ({computeError})");
}
if (name.StartsWith("mrt", StringComparison.Ordinal))
{
- (uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
+ Gen5PixelOutputBinding[] pixelOutputs = name switch
{
- "mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[]
- {
- (0, 0, "Float"),
- (3, 1, "Uint"),
- (6, 2, "Sint"),
- },
- "mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")],
- "mrt8" => Enumerable.Range(0, 8)
- .Select(index => ((uint)index, (uint)index, "Float"))
- .ToArray(),
- _ => [(0, 0, "Float")],
+ "mrt" =>
+ [
+ new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(3, 1, Gen5PixelOutputKind.Uint),
+ new Gen5PixelOutputBinding(6, 2, Gen5PixelOutputKind.Sint),
+ ],
+ "mrt-float2" =>
+ [
+ new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(1, 1, Gen5PixelOutputKind.Float),
+ ],
+ "mrt8" =>
+ [
+ new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(1, 1, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(2, 2, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(3, 3, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(4, 4, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(5, 5, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(6, 6, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(7, 7, Gen5PixelOutputKind.Float),
+ ],
+ _ => [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
};
- var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length);
- for (var index = 0; index < outputSpecs.Length; index++)
- {
- var spec = outputSpecs[index];
- pixelOutputs.SetValue(
- Activator.CreateInstance(
- pixelOutputBindingType,
- spec.GuestSlot,
- spec.HostLocation,
- Enum.Parse(pixelOutputKindType, spec.Kind)),
- index);
- }
- var pixelArgs = PadWithDefaults(
- tryCompilePixel,
- [state, evaluation, pixelOutputs, null, null]);
- if ((bool)tryCompilePixel.Invoke(
- null,
- BindingFlags.OptionalParamBinding,
- null,
- pixelArgs,
- null)!)
+ if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, pixelOutputs, out var pixelShader, out var pixelError))
{
- var shader = pixelArgs[3]!;
- var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
- File.WriteAllBytes(path, spirv);
- Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
+ File.WriteAllBytes(path, pixelShader.Spirv);
+ Console.WriteLine($"[{name}] pixel emit: success, {pixelShader.Spirv.Length} bytes -> {path}");
}
else
{
failures++;
- Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
+ Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelError})");
}
if (name == "mrt")
{
- var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2);
- invalidOutputs.SetValue(
- Activator.CreateInstance(
- pixelOutputBindingType,
- 0u,
- 0u,
- Enum.Parse(pixelOutputKindType, "Float")),
- 0);
- invalidOutputs.SetValue(
- Activator.CreateInstance(
- pixelOutputBindingType,
- 3u,
- 7u,
- Enum.Parse(pixelOutputKindType, "Float")),
- 1);
- var invalidPixelArgs = PadWithDefaults(
- tryCompilePixel,
- [state, evaluation, invalidOutputs, null, null]);
- if ((bool)tryCompilePixel.Invoke(
- null,
- BindingFlags.OptionalParamBinding,
- null,
- invalidPixelArgs,
- null)!)
+ Gen5PixelOutputBinding[] invalidOutputs =
+ [
+ new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float),
+ new Gen5PixelOutputBinding(3, 7, Gen5PixelOutputKind.Float),
+ ];
+ if (Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, invalidOutputs, out _, out var invalidError))
{
failures++;
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
}
else
{
- Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
+ Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidError})");
}
}
}
@@ -390,37 +293,6 @@ Console.WriteLine(failures == 0
: $"RESULT: {failures} unexpected outcome(s)");
Environment.ExitCode = failures == 0 ? 0 : 1;
-// Reflection Invoke does not apply C# default parameter values, so a newly
-// added optional parameter on a translator entry point would otherwise throw
-// TargetParameterCountException. Type.Missing + OptionalParamBinding lets the
-// runtime substitute the declared defaults; only a new *required* parameter
-// should force a tool update.
-static object?[] PadWithDefaults(MethodInfo method, object?[] arguments)
-{
- var parameters = method.GetParameters();
- if (arguments.Length > parameters.Length)
- {
- throw new InvalidOperationException(
- $"{method.DeclaringType?.Name}.{method.Name} takes fewer parameters than the tool supplies");
- }
-
- var padded = new object?[parameters.Length];
- arguments.CopyTo(padded, 0);
- for (var i = arguments.Length; i < padded.Length; i++)
- {
- if (!parameters[i].IsOptional)
- {
- throw new InvalidOperationException(
- $"{method.DeclaringType?.Name}.{method.Name} gained a required parameter " +
- $"'{parameters[i].Name}' — the tool needs updating");
- }
-
- padded[i] = Type.Missing;
- }
-
- return padded;
-}
-
internal sealed class FakeMemory : ICpuMemory
{
private readonly List<(ulong Base, byte[] Data)> _regions = [];
diff --git a/tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj b/tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj
index 5cf2ea0..2db6e3e 100644
--- a/tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj
+++ b/tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj
@@ -12,8 +12,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
false
+
-
+
+