From 30fdd8d6ed64e0140c98bf7f8efcc94a163a77ff Mon Sep 17 00:00:00 2001 From: Gutemberg Ribeiro Date: Wed, 15 Jul 2026 18:11:24 +0100 Subject: [PATCH] [Gpu] Backend-neutral shader compiler and guest-GPU renderer seam (#200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ShaderCompiler] Extract the backend-neutral shader compiler project Move the Gen5 (gfx10) microcode decoder, the scalar evaluator, the shader IR, and the metadata reader out of SharpEmu.Libs/Agc into a new SharpEmu.ShaderCompiler project — the half of shader compilation every codegen backend (SPIR-V today; MSL and DXIL later) consumes. Types go public: they are the contract now. Nothing in the project may depend on a host graphics API; the SPIR-V-specific artifact types (Gen5SpirvShader, Gen5SpirvStage) stay beside the emitter in Libs. Three couplings surfaced by the move, each resolved at the right depth: GuestDrawKind was defined inside VulkanVideoPresenter despite being a guest-domain, decoder-produced concept — it moves to the shared project; the evaluator's one HLE dependency (the tracked-libc-heap read fallback) becomes an injectable hook that a Libs module initializer installs before any caller can reach the evaluator; and the inline- constant table is promoted to a shared Gen5InlineConstants so backends cannot drift on constant semantics (the SPIR-V translator now delegates to it). The ShaderDump tool drops its reflection over the moved types in favor of direct typed calls; only the SPIR-V emitter, still internal to Libs until it moves to its own backend project, is reached via reflection. Verified by a clean solution build, the existing test suite, and a full ShaderDump conformance run. * [ShaderCompiler] Move the SPIR-V emitter into SharpEmu.ShaderCompiler.Vulkan Gen5SpirvTranslator (with its ALU partial), SpirvModuleBuilder, SpirvFixedShaders, and the Gen5SpirvShader/Gen5SpirvStage artifact types move whole from SharpEmu.Libs/Agc into the first per-backend codegen project. Notably it needs no Vulkan bindings reference: emitters produce bytes from the shared IR; renderers own graphics APIs. Types go public as the backend's contract; AgcExports and the presenter consume them exactly as before. The ShaderDump tool drops its last reflection: with both halves of the pipeline public it drives decode and all three emit entry points with direct typed calls, retiring the PadWithDefaults invoke shim — and it no longer references SharpEmu.Libs at all, making the conformance tool emulator-independent by design. Verified by a clean solution build, the test suite, a full ShaderDump conformance run, and a locked-mode restore under the pinned SDK. * [Gpu] Extract the guest-GPU backend seam (IGuestGpuBackend) The AGC/VideoOut/SystemService export layers now reach the renderer through IGuestGpuBackend via GuestGpu.Current (mirroring HostPlatform), instead of calling VulkanVideoPresenter statics. The Vulkan backend is a thin adapter over the existing presenter, so the extraction stays mechanical; only the adapter and the presenter itself reference the presenter now. The types crossing the seam move to Gpu/GuestGpuTypes.cs and drop their Vulkan prefixes, which an audit showed were misnomers: every field is a neutral primitive or a raw guest value (guest addresses, format and number-type codes, CB_BLEND register bitfields, verbatim sampler descriptor dwords). The one genuine Vulkan value in the old surface — the Silk.NET Format inside VulkanRenderTargetFormat, which callers never read — stops crossing: TryDecodeRenderTargetFormat is replaced at the seam by TryGetRenderTargetOutputKind, which surfaces only the Gen5PixelOutputKind callers actually consume, keeping native formats a backend-internal concern. ToVulkanSampler in AgcExports is renamed ToGuestSampler to match what it always produced. Seam rules are documented on the interface: no host-API value crosses, and submission stays coarse-grained with synchronization internal to backends. Interim exception, resolved next: shader parameters are still SPIR-V blobs. * [Gpu] Move shader compilation behind the guest-GPU backend The seam's interim exception is gone: AgcExports no longer calls Gen5SpirvTranslator or handles SPIR-V bytes. IGuestGpuBackend gains the three TryCompile entry points, which take the backend-neutral (Gen5ShaderState, Gen5ShaderEvaluation) contract plus the flat per-role resource-slot bases a multi-stage draw needs, and return opaque IGuestCompiledShader handles that only the producing backend can submit — the Vulkan backend wraps its SPIR-V in VulkanCompiledGuestShader and rejects foreign handles loudly. Draw and dispatch submissions take handles instead of byte arrays; the shader caches in AgcExports store handles. IGuestCompiledShader.Payload exposes the backend-defined compiled bytes for exactly two callers: the diagnostics dump and the size trace — documented as never-interpret. The unused _pixelSpirvCache is deleted. With this, a Metal or DX12 backend plugs in by implementing IGuestGpuBackend with its own codegen; nothing in the export layers knows which shader format exists. Verified by a clean solution build, the test suite, and a full ShaderDump conformance run under the pinned SDK. * [Gpu] Fix rename collateral from the seam extraction Address review findings: a doc comment picked up the mechanical VulkanVideoPresenter -> GuestGpu.Current rewrite and ended up naming members that do not exist on the interface, and CreateVulkanIndexBuffer kept its Vulkan prefix while every sibling factory was de-Vulkanized — it produces the neutral GuestIndexBuffer, so it is CreateGuestIndexBuffer. * [Gpu] Label diagnostics dumps with the backend's payload extension Address the review's altitude finding on DumpSpirv: the dump helper's IR-disassembly half is backend-neutral and stays put, but writing the opaque payload to a hardcoded .spv interpreted bytes the seam says never to interpret. IGuestCompiledShader now declares its payload's file extension, and the renamed DumpCompiledShader takes the handle and writes honestly-labeled dumps whichever backend produced them. * [Gpu] Make the shader-cache hit path allocation-free and lock-free Every translated draw built its cache key with a LINQ Select feeding string.Join plus one interpolated string per render target — steady per-draw allocation whether or not the shaders were already cached. The output layout is now packed exactly into a ulong (guest slot in 6 bits + output kind in 2 bits per target, host locations being the byte positions, target count in the key beside it), and the Gen5PixelOutputBinding array is only materialized on a cache miss, where compilation dwarfs it. The graphics/compute shader caches switch from Dictionary guarded by _submitTraceGate to ConcurrentDictionary, making the per-draw and per-dispatch hit paths lock-free and decoupling them from the tracing gate they coincidentally shared. And the seam-shaped render-target list is built once when a translated draw is created instead of a Select/ToArray per submission of a cached draw. * [Gpu] Replace LINQ with explicit loops in code this branch introduced Project rule going forward: no LINQ — it allocates enumerators, closures, and delegates, and this codebase is GC-pause-sensitive. The pixel-output and guest-render-target array builds and the ShaderDump store-PC collection become plain loops; pre-existing LINQ elsewhere is left for changes that already touch those lines. * [ShaderCompiler] Suppress CA2255 on the evaluator hook installer The analyzer coverage that arrived with the rebase flags ModuleInitializer in library code; 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 — so suppress with that justification rather than weaken the guarantee to a static constructor's lazier timing. * [Gpu] Resolve rebase artifacts onto main Dedupe the System.Collections.Concurrent using in AgcExports that the rebase merge duplicated (main and this branch each added it), and regenerate the lock files for the new shader-compiler projects and SharpEmu.Libs against main's current package graph so --locked-mode restore matches at the branch tip. * [CI] Comment per-platform build artifact links on PRs Adds a workflow_run workflow that, after "Build and Release" finishes a pull-request build, posts (and keeps updated in place) a single PR comment linking the Windows, Linux, and macOS artifacts from that run. It runs via workflow_run rather than in the build workflow because PRs from forks build with a read-only token that cannot comment; the follow-on run executes in the base-repo context with write access and without checking out fork code. GitHub only triggers workflow_run from the default branch, so this takes effect once merged to main. --- .github/workflows/pr-build-links.yml | 105 +++++++ SharpEmu.slnx | 2 + src/SharpEmu.CLI/packages.lock.json | 26 +- src/SharpEmu.Core/packages.lock.json | 18 +- src/SharpEmu.Libs/Agc/AgcExports.cs | 278 +++++++++--------- .../Agc/AgcShaderCompilerHooks.cs | 30 ++ src/SharpEmu.Libs/Gpu/GuestGpu.cs | 18 ++ src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs | 116 ++++++++ src/SharpEmu.Libs/Gpu/IGuestCompiledShader.cs | 19 ++ src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs | 140 +++++++++ .../Gpu/Vulkan/VulkanCompiledGuestShader.cs | 12 + .../Gpu/Vulkan/VulkanGuestGpuBackend.cs | 251 ++++++++++++++++ src/SharpEmu.Libs/SharpEmu.Libs.csproj | 2 + .../SystemService/SystemServiceExports.cs | 3 +- src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 11 +- .../VideoOut/VulkanVideoPresenter.cs | 260 +++++----------- src/SharpEmu.Libs/packages.lock.json | 14 +- .../Gen5SpirvShader.cs | 23 ++ .../Gen5SpirvTranslator.Alu.cs | 50 +--- .../Gen5SpirvTranslator.cs | 6 +- .../SharpEmu.ShaderCompiler.Vulkan.csproj | 19 ++ .../SpirvFixedShaders.cs | 4 +- .../SpirvModuleBuilder.cs | 22 +- .../packages.lock.json | 22 ++ .../Gen5InlineConstants.cs | 54 ++++ .../Gen5ShaderIr.cs | 70 ++--- .../Gen5ShaderMetadataReader.cs | 4 +- .../Gen5ShaderScalarEvaluator.cs | 32 +- .../Gen5ShaderTranslator.cs | 11 +- src/SharpEmu.ShaderCompiler/GuestDrawKind.cs | 15 + .../SharpEmu.ShaderCompiler.csproj | 20 ++ .../packages.lock.json | 16 + tests/SharpEmu.Libs.Tests/packages.lock.json | 24 +- tools/SharpEmu.Tools.ShaderDump/Program.cs | 252 ++++------------ .../SharpEmu.Tools.ShaderDump.csproj | 5 +- 35 files changed, 1308 insertions(+), 646 deletions(-) create mode 100644 .github/workflows/pr-build-links.yml create mode 100644 src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs create mode 100644 src/SharpEmu.Libs/Gpu/GuestGpu.cs create mode 100644 src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs create mode 100644 src/SharpEmu.Libs/Gpu/IGuestCompiledShader.cs create mode 100644 src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs create mode 100644 src/SharpEmu.Libs/Gpu/Vulkan/VulkanCompiledGuestShader.cs create mode 100644 src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs create mode 100644 src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvShader.cs rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler.Vulkan}/Gen5SpirvTranslator.Alu.cs (98%) rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler.Vulkan}/Gen5SpirvTranslator.cs (99%) create mode 100644 src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler.Vulkan}/SpirvFixedShaders.cs (98%) rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler.Vulkan}/SpirvModuleBuilder.cs (98%) create mode 100644 src/SharpEmu.ShaderCompiler.Vulkan/packages.lock.json create mode 100644 src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler}/Gen5ShaderIr.cs (80%) rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler}/Gen5ShaderMetadataReader.cs (97%) rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler}/Gen5ShaderScalarEvaluator.cs (98%) rename src/{SharpEmu.Libs/Agc => SharpEmu.ShaderCompiler}/Gen5ShaderTranslator.cs (99%) create mode 100644 src/SharpEmu.ShaderCompiler/GuestDrawKind.cs create mode 100644 src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj create mode 100644 src/SharpEmu.ShaderCompiler/packages.lock.json 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 + - + +