// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Xunit;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
///
/// Structural checks over the emitted MSL — these run on every platform because
/// translation is pure text generation; only the runtime tests need a Metal device.
///
public sealed class MslTranslationTests
{
[Fact]
public void EveryFixtureTranslates()
{
foreach (var fixture in Gen5ComputeFixtures.All)
{
var shader = Gen5ComputeFixtures.CompileOrThrow(fixture);
Assert.Equal(Gen5MslStage.Compute, shader.Stage);
Assert.Equal("gen5_cs", shader.EntryPoint);
Assert.Contains("kernel void gen5_cs(", shader.Source, StringComparison.Ordinal);
Assert.Contains("while (active)", shader.Source, StringComparison.Ordinal);
}
}
[Fact]
public void ExecMaskedStoresAreGuarded()
{
var shader = Gen5ComputeFixtures.CompileOrThrow(Gen5ComputeFixtures.ExecStore);
// Every buffer store must sit behind the per-lane EXEC guard.
Assert.Contains("if (exec)", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_store_bytes(b0,", shader.Source, StringComparison.Ordinal);
// s_mov_b32 exec_lo, 0 / -1 must drive the per-lane bool.
Assert.Contains("exec = ((", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void LoopFixtureProducesMultipleDispatcherBlocks()
{
var shader = Gen5ComputeFixtures.CompileOrThrow(Gen5ComputeFixtures.Loop);
// The backward branch splits the program into at least three blocks and
// the conditional branch selects between loop head and fallthrough.
Assert.Contains("case 0u:", shader.Source, StringComparison.Ordinal);
Assert.Contains("case 1u:", shader.Source, StringComparison.Ordinal);
Assert.Contains("case 2u:", shader.Source, StringComparison.Ordinal);
Assert.Contains("pc = (scc) ?", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void DispatcherIsBoundedByDefault()
{
var shader = Gen5ComputeFixtures.CompileOrThrow(Gen5ComputeFixtures.Fmac);
Assert.Contains("if (++steps >=", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void UniformsCarryDispatchLimitAndBufferLengths()
{
var shader = Gen5ComputeFixtures.CompileOrThrow(Gen5ComputeFixtures.ExecStore);
Assert.Contains("struct SharpEmuUniforms", shader.Source, StringComparison.Ordinal);
Assert.Contains("dispatch_limit_x", shader.Source, StringComparison.Ordinal);
Assert.Contains("buffer_bytes[", shader.Source, StringComparison.Ordinal);
// One global binding: b0 at [[buffer(0)]], uniforms at [[buffer(1)]].
Assert.Contains("device uint* b0 [[buffer(0)]]", shader.Source, StringComparison.Ordinal);
Assert.Contains("[[buffer(1)]]", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void PixelStageEmitsFragmentInterface()
{
var shader = Gen5ComputeFixtures.CompilePixelOrThrow();
Assert.Equal(Gen5MslStage.Pixel, shader.Stage);
Assert.Equal("gen5_ps", shader.EntryPoint);
Assert.Equal(1u, shader.AttributeCount);
Assert.Contains("fragment Gen5PsOut gen5_ps(", shader.Source, StringComparison.Ordinal);
Assert.Contains("float4 attr0 [[user(locn0)]];", shader.Source, StringComparison.Ordinal);
Assert.Contains("[[color(0)]]", shader.Source, StringComparison.Ordinal);
Assert.Contains("[[position]]", shader.Source, StringComparison.Ordinal);
// Interpolation reads land in VGPRs; the export writes MRT0 under EXEC
// and inactive lanes discard at the end.
Assert.Contains("as_type(sharpemu_in.attr0[0])", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_out.mrt0 = exec ?", shader.Source, StringComparison.Ordinal);
Assert.Contains("discard_fragment();", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void PixelOutputKindsSelectTheAttachmentType()
{
var uintShader = Gen5ComputeFixtures.CompilePixelOrThrow(Gen5PixelOutputKind.Uint);
Assert.Contains("uint4 mrt0 [[color(0)]];", uintShader.Source, StringComparison.Ordinal);
var sintShader = Gen5ComputeFixtures.CompilePixelOrThrow(Gen5PixelOutputKind.Sint);
Assert.Contains("int4 mrt0 [[color(0)]];", sintShader.Source, StringComparison.Ordinal);
}
[Fact]
public void VertexStageEmitsVertexInterface()
{
var shader = Gen5ComputeFixtures.CompileVertexOrThrow();
Assert.Equal(Gen5MslStage.Vertex, shader.Stage);
Assert.Equal("gen5_vs", shader.EntryPoint);
Assert.Equal(1u, shader.AttributeCount);
Assert.Contains("vertex Gen5VsOut gen5_vs(", shader.Source, StringComparison.Ordinal);
Assert.Contains("float4 sharpemu_position [[position]];", shader.Source, StringComparison.Ordinal);
Assert.Contains("float4 param0 [[user(locn0)]];", shader.Source, StringComparison.Ordinal);
Assert.Contains("uint sharpemu_vertex_id [[vertex_id]],", shader.Source, StringComparison.Ordinal);
Assert.Contains("v[5] = sharpemu_vertex_id;", shader.Source, StringComparison.Ordinal);
Assert.Contains("v[8] = sharpemu_instance_id;", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_out.sharpemu_position = exec ?", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_out.param0 = exec ?", shader.Source, StringComparison.Ordinal);
Assert.Contains("return sharpemu_out;", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void RequiredVertexOutputsAreZeroFilledDeclarations()
{
// The paired fragment shader reads locations 0..2; the program only
// exports param0, so 1 and 2 must still be declared (zero-filled).
var shader = Gen5ComputeFixtures.CompileVertexOrThrow(requiredVertexOutputCount: 3);
Assert.Equal(3u, shader.AttributeCount);
Assert.Contains("float4 param1 [[user(locn1)]];", shader.Source, StringComparison.Ordinal);
Assert.Contains("float4 param2 [[user(locn2)]];", shader.Source, StringComparison.Ordinal);
}
[Fact]
public void FixedShadersCoverThePresenterSurface()
{
var fullscreen = MslFixedShaders.CreateFullscreenVertex(2);
Assert.Contains("vertex FullscreenOut fullscreen_vs(", fullscreen, StringComparison.Ordinal);
Assert.Contains("float4 attr1 [[user(locn1)]];", fullscreen, StringComparison.Ordinal);
Assert.Contains("tex0.sample(smp0, in.attr0.xy)", MslFixedShaders.CreateCopyFragment(), StringComparison.Ordinal);
Assert.Contains("float4(1.0f, 0.0f, 1.0f, 1.0f)", MslFixedShaders.CreateSolidFragment(1f, 0f, 1f, 1f), StringComparison.Ordinal);
Assert.Contains("return in.attr3;", MslFixedShaders.CreateAttributeFragment(3), StringComparison.Ordinal);
Assert.Contains("fragment void depth_only_fs()", MslFixedShaders.CreateDepthOnlyFragment(), StringComparison.Ordinal);
}
[Fact]
public void UnsupportedOpcodeFailsLoudlyWithPc()
{
// v_cubeid_f32 is real but outside the phase-1 ALU set: the translator
// must name the opcode and pc instead of emitting wrong code.
var fixture = new Gen5ComputeFixture(
"unsupported",
[
0xD5C40000, 0x04060501, // v_cubeid_f32 v0, v1, v2, v3
0xBF810000, // s_endpgm
],
StoreScalarResourceBase: 0,
StoreBackingBytes: 0);
var exception = Assert.Throws(
() => Gen5ComputeFixtures.CompileOrThrow(fixture));
Assert.Contains("pc=0x", exception.Message, StringComparison.Ordinal);
}
}