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 + - + +